深度学习 Day 10——使用 Python 和 TensorFlow 构建深度音频分类器

深度学习 Day 10——使用 Python 和 TensorFlow 构建深度音频分类器

一、前言

经过长达21天的学习挑战赛的学习,我也取得了一些成就,获得了学习达人奖,这让我对在学习又上进了一点,希望以后CSDN官方能出一些这类型的活动。

这是我的学习专栏:Python深度学习专栏

我也有一段时间没有更新有关深度学习方面的博客了,一直没有找到合适的,今天找到了一个网站,它是有关数据处理的,刚好有一个挑战正是需要通过深度学习来解决的,那我们一起来看看叭!

在这里插入图片描述

以上就是本次的深度学习的目标,数据集可以直接在网站下面下载的。

二、我的环境

  • 电脑系统:Windows 11
  • 语言环境:Python 3.8.5
  • 编译器:PyCharm 2022.2
  • 深度学习环境:TensorFlow 2.3.4
  • 显卡及显存:RTX 3070 8G

三、准备工作

1、导入库

import os
from matplotlib import pyplot as plt
import tensorflow as tf 
import tensorflow_io as tfio

2、设置GPU

如果电脑性能很好的话推荐使用GPU,一般还是使用CPU,如果使用CPU的话下面代码可以忽略。

gpus = tf.config.list_physical_devices("GPU")

if gpus:
    tf.config.experimental.set_memory_growth(gpus[0], True)  #设置GPU显存用量按需使用
    tf.config.set_visible_devices([gpus[0]], "GPU")

本期博客在博主的测试过程中发现完全使用GPU的话电脑会爆显存,所以推荐使用CPU进行,利用GPU加速,添加这个在前面即可。

os.environ["CUDA_VISIBLE_DEVICES"] = "-1"

3、构建数据加载功能

  • 定义文件路径

    这里我们使用两个个文件进行测试:

    CAPUCHIN_FILE = 'E:/DL_data/Parsed_Capuchinbird_Clips/XC3776-5.wav'
    NOT_CAPUCHIN_FILE = 'E:/DL_data/Parsed_Not_Capuchinbird_Clips/afternoon-birds-song-in-forest-5.wav'
    
  • 数据预处理

    对文件进行处理并将其转换为16赫兹,然后获得一个单通道,传入文件并加载它并将其转换为波形,得到一个字节编码的字符串,然后将其解码,获得一个单声道音频。删除尾随轴并将音频信号幅度下降。

    def load_wav_16k_mono(filename):
        # 加载编码的 wav 文件
        file_contents = tf.io.read_file(filename)
        # 解码 wav(通道张量)
        wav, sample_rate = tf.audio.decode_wav(file_contents, desired_channels=1)
        # 删除尾随轴
        wav = tf.squeeze(wav, axis=-1)
        sample_rate = tf.cast(sample_rate, dtype=tf.int64)
        # 从 44100Hz 到 16000Hz - 音频信号的幅度
        wav = tfio.audio.resample(wav, rate_in=sample_rate, rate_out=16000)
        return wav
    
  • 可视化绘制波型图

    wave = load_wav_16k_mono(CAPUCHIN_FILE)
    nwave = load_wav_16k_mono(NOT_CAPUCHIN_FILE)
    
    plt.plot(wave)
    plt.plot(nwave)
    plt.show()
    

    绘制的图形如下:
    在这里插入图片描述

四、创建TensorFlow数据集

1、定义正负数据的路径

前面只是使用了单个文件进行测试,现在需要设置整个文件夹

POS = "E:\DL_data\Parsed_Capuchinbird_Clips"
NEC = "E:\DL_data\Parsed_Not_Capuchinbird_Clips"

2、创建数据集

使用list_files方法获取一组文件即数据集

pos = tf.data.Dataset.list_files(POS+'\*.wav')
neg = tf.data.Dataset.list_files(NEG+'\*.wav')

我们打印其中一个:

print(pos.as_numpy_iterator().next())

它运行的结果是:

b'E:\\DL_data\\Parsed_Capuchinbird_Clips\\XC22397-1.wav'

我们打印正面例子和负面例子:

print(tf.ones(len(pos)))
print(tf.zeros(len(neg)))

它运行的结果是;

tf.Tensor(
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
 1.], shape=(217,), dtype=float32)
tf.Tensor(
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.], shape=(593,), dtype=float32)

可以看出这是一个不平衡的数据集,理想情况下,为了提高性能的方法就是平衡数据集,来有效的重新采样和负采样

3、添加标签并合并正负样本

将正面样本跟负面样本连接在一起,正面是1.0,负面是0.0,然后共同存储在一个变量里面

positives = tf.data.Dataset.zip((pos, tf.data.Dataset.from_tensor_slices(tf.ones(len(pos)))))
negatives = tf.data.Dataset.zip((neg, tf.data.Dataset.from_tensor_slices(tf.zeros(len(neg)))))
data = positives.concatenate(negatives)

五、确定卷尾鸟叫声的平均长度

1、确定平均核心长度

计算每个独立的时间有多长,因为每一个音频的时间不等。我们要确保捕获的大部分波在特定的分类形式中。

lengths = []
for file in os.listdir(POS):
    tensor_wave = load_wav_16k_mono(os.path.join(POS, file))
    lengths.append(len(tensor_wave))

我们可以通过length来计算最大长度和最小长度。

2、计算平均值、最小值和最大值

tf.math.reduce_mean(lengths)
tf.math.reduce_min(lengths)
tf.math.reduce_max(lengths)

print(tf.math.reduce_mean(lengths))
print(tf.math.reduce_min(lengths))
print(tf.math.reduce_max(lengths))

它运行的结果时:

tf.Tensor(54156, shape=(), dtype=int32)
tf.Tensor(32000, shape=(), dtype=int32)
tf.Tensor(80000, shape=(), dtype=int32)

我们可以取大约48000个示例来进行训练。

六、构建预处理函数以转换为频谱图

1、构建预处理函数

def preprocess(file_path, label):
    wav = load_wav_16k_mono(file_path)
    wav = wav[:48000]
    zero_padding = tf.zeros([48000] - tf.shape(wav), dtype=tf.float32)
    # 给使用tf.concat方法覆盖它在其开头添加0确保所有的都满足48000个样本长度
    wav = tf.concat([zero_padding, wav],0)
    # 创建频谱图,指定帧长度和帧步长
    spectrogram = tf.signal.stft(wav, frame_length=320, frame_step=32)
    # 将其转换为绝对值
    spectrogram = tf.abs(spectrogram)
    spectrogram = tf.expand_dims(spectrogram, axis=2)
    print(spectrogram)
    return spectrogram, label

我们打印频谱图:

tf.Tensor(
[[[7.99823925e-03]
  [8.25822540e-03]
  [1.84503011e-02]
  ...
  [2.84427824e-05]
  [1.35974824e-05]
  [2.94670463e-06]]
 [[9.83100943e-03]
  [4.08219127e-03]
  [2.13126298e-02]
  ...
  [2.19040503e-05]
  [1.61567987e-05]
  [3.11620533e-06]]
 [[6.63647614e-03]
  [1.08020864e-02]
  [2.46545188e-02]
  ...
  [6.17923661e-06]
  [2.39560177e-05]
  [2.69152224e-06]]
 ...
 [[4.02274132e-02]
  [5.40111288e-02]
  [4.48272154e-02]
  ...
  [2.90782573e-05]
  [1.20027025e-05]
  [7.91996717e-06]]
 [[5.65412566e-02]
  [5.55172227e-02]
  [4.53842059e-02]
  ...
  [9.91229717e-06]
  [1.25206425e-05]
  [1.64099038e-06]]
 [[5.50268032e-02]
  [4.98828664e-02]
  [4.67262156e-02]
  ...
  [8.93952983e-06]
  [1.23461577e-05]
  [9.84594226e-06]]], shape=(1491, 257, 1), dtype=float32)

可以看出它是一个1491像素×257像素的通道。

2、测试功能和绘制频谱图

filepath, label = positives.shuffle(buffer_size=10000).as_numpy_iterator().next()
spectrogram, label = preprocess(filepath, label)
plt.figure(figsize=(30, 20))
plt.imshow(tf.transpose(spectrogram)[0])
plt.show()

绘制的正面样本频谱图是:

在这里插入图片描述

这就是原始波形,我们将其转化成了一个图像,这是正面样本的频谱图,我们来看看负面样本的频谱图跟正面样本的有什么区别:

filepath, label = negatives.shuffle(buffer_size=10000).as_numpy_iterator().next()
spectrogram, label = preprocess(filepath, label)
plt.figure(figsize=(30, 20))
plt.imshow(tf.transpose(spectrogram)[0])
plt.show()

绘制负面样本的频谱图:

在这里插入图片描述

可以看出两者差距很大,卷尾鸟的声音总是先低再高这种趋势。

七、创建训练和测试分区

1、创建TensorFlow数据管道

通过一个频谱图的方法运行,预处理tensorflow数据管道,映射然后将其正负样本进行混合,避免过度拟合,并且我们在模型中引入不必要的偏差,然后对其进行批处理,一次训练16个样本,然后进行预取8个样本。

data = data.map(preprocess)
data = data.cache()
data = data.shuffle(buffer_size=1000)
data = data.batch(16)
data = data.prefetch(8)

2、拆分为训练和测试分区

train = data.take(36)
test = data.skip(36).take(15)

3、测试一批数据

samples, labels = train.as_numpy_iterator().next()
print(samples.shape)

我们来查看一下samples.shape:

(16, 1491, 257, 1)

可以看出我们有16个不同的频谱图示例,它们都是1491×257的形状,这个特定的形状将作为我们深度学习模型的输入。

八、构建深度学习模型

1、导入构建模型库

from tensorflow.keras.layers import Conv2D, Dense, Flatten # 引入卷积层到密集层

2、构建顺序模型并查看参数

def build_model():
    inputs = tf.keras.Input(shape=(1491, 257, 1))
    # # 添加卷积层
    out = Conv2D(16, (7, 7), strides=3, activation='relu')(inputs)
    out = Conv2D(16, (7, 7), strides=3, activation='relu')(out)
    # 展平
    out = Flatten()(out)
    # 添加密集层
    out = Dense(128, activation='relu')(out)
    out = Dense(1, activation='sigmoid')(out)
    return tf.keras.Model(inputs=inputs, outputs=out)


model = build_model()
model.compile('Adam', loss='BinaryCrossentropy', metrics=[tf.keras.metrics.Recall(), tf.keras.metrics.Precision()])
model.summary()
#print(model.summary())

它运行的结果是:

Model: "functional_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 1491, 257, 1)]    0         
_________________________________________________________________
conv2d (Conv2D)              (None, 495, 84, 16)       800       
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 163, 26, 16)       12560     
_________________________________________________________________
flatten (Flatten)            (None, 67808)             0         
_________________________________________________________________
dense (Dense)                (None, 128)               8679552   
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 129       
=================================================================
Total params: 8,693,041
Trainable params: 8,693,041
Non-trainable params: 0

这是一个神经网络,它非常的大,拥有8.6百万参数。

3、拟合模型、查看损失和KPI图

# 指定训练参数
hist = model.fit(train, epochs=4, validation_data=test)

plt.title('Loss')
plt.plot(hist.history['loss'], 'r')
plt.plot(hist.history['val_loss'], 'b')
plt.show()

plt.title('Precision')
plt.plot(hist.history['precision'], 'r')
plt.plot(hist.history['val_precision'], 'b')
plt.show()

plt.title('Recall')
plt.plot(hist.history['recall'], 'r')
plt.plot(hist.history['val_recall'], 'b')
plt.show()

它运行的结果是:

Epoch 1/4
36/36 [==============================] - 3s 76ms/step - loss: 0.5493 - recall: 0.7632 - precision: 0.7073 - val_loss: 0.2875 - val_recall: 1.0000 - val_precision: 0.9048
Epoch 2/4
36/36 [==============================] - 3s 73ms/step - loss: 0.2301 - recall: 0.9250 - precision: 0.9024 - val_loss: 0.0891 - val_recall: 0.9231 - val_precision: 0.9231
Epoch 3/4
36/36 [==============================] - 3s 73ms/step - loss: 0.1134 - recall: 0.9444 - precision: 0.9444 - val_loss: 0.0468 - val_recall: 0.9500 - val_precision: 1.0000
Epoch 4/4
36/36 [==============================] - 3s 73ms/step - loss: 0.1385 - recall: 0.9778 - precision: 0.9778 - val_loss: 0.0420 - val_recall: 1.0000 - val_precision: 1.0000

可以看出最后获得了百分之百的召回率和百分之百的精度,如果没有达到这个标准可以添加训练次数。

它绘制的结果是:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

九、对单个剪辑进行预测

1、获取一批数据并做出预测

X_test, y_test = test.as_numpy_iterator().next()
yhat = model.predict(X_test)

2、将 Logits 转换为类

yhat = []
for prediction in yhat:
    if prediction > 0.5:
        yhat.append(1)
    else:
        yhat.append(0)

你也可以进行将上面进行简写:

yhat = [1 if prediction > 0.5 else 0 for prediction in yhat]

我们可以看一下它在随机一批数据中听到了多少个卷尾鸟的叫声:

print(tf.math.reduce_sum(yhat))

它运行的结果是:

<tf.Tensor: shape=(), dtype=int32, numpy=7>

然后我们在看一下我们的预测结果:

print(tf.math.reduce_sum(y_test))

它运行的结果是:

<tf.Tensor: shape=(), dtype=int32, numpy=7.0>

可以看出预测跟实际一样,我们的预测是正确的,这非常的棒。

十、构建森林解析函数

1、加载 MP3

def load_mp3_16k_mono(filename):
    """ 
    加载 WAV 文件,将其转换为浮点张量,重新采样为 16 kHz 单声道音频。 
    """
    res = tfio.audio.AudioIOTensor(filename)
    # 转换为张量并组合通道
    tensor = res.to_tensor()
    tensor = tf.math.reduce_sum(tensor, axis=1) / 2
    # 提取采样率和投射
    sample_rate = res.rate
    sample_rate = tf.cast(sample_rate, dtype=tf.int64)
    # 重采样到 16 kHz
    wav = tfio.audio.resample(tensor, rate_in=sample_rate, rate_out=16000)
    return wav

mp3 = 'E:/DL_data/Forest Recordings/recording_00.mp3'
# 把这个一大片段分割成与我们传递给模型的相同音频相同大小的音频片段并作出预测
wav = load_mp3_16k_mono(mp3)
audio_slices = tf.keras.preprocessing.timeseries_dataset_from_array(wav, wav, sequence_length=48000, sequence_stride=48000, batch_size=1)
samples, index = audio_slices.as_numpy_iterator().next()

2、将剪辑转换为窗口频谱图的构建函数

def preprocess_mp3(sample, index):
    sample = sample[0]
    zero_padding = tf.zeros([48000] - tf.shape(sample), dtype=tf.float32)
    wav = tf.concat([zero_padding, sample],0)
    spectrogram = tf.signal.stft(wav, frame_length=320, frame_step=32)
    spectrogram = tf.abs(spectrogram)
    spectrogram = tf.expand_dims(spectrogram, axis=2)
    return spectrogram

3、将较长的剪辑转换为窗口并进行预测

# 将音频切片转换成频谱图
audio_slices = tf.keras.preprocessing.timeseries_dataset_from_array(wav, wav, sequence_length=48000, sequence_stride=48000, batch_size=1)
audio_slices = audio_slices.map(preprocess_mp3)
audio_slices = audio_slices.batch(64)
yhat = model.predict(audio_slices)
yhat = [1 if prediction > 0.99 else 0 for prediction in yhat]

我们打印一下预测的结果:

print(yhat)

它运行的结果是:

[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

可以看出检测出了7个卷尾鸟的叫声。

4、组连续检测

from itertools import groupby
yhat = [key for key, group in groupby(yhat)]
calls = tf.math.reduce_sum(yhat).numpy()

十一、作出预测

1、循环播放所有录音并做出预测

results = {}
for file in os.listdir('E:\DL_data\Forest Recordings'):
    FILEPATH = os.path.join('E:\DL_data\Forest Recordings', file)
    wav = load_mp3_16k_mono(FILEPATH)
    audio_slices = tf.keras.preprocessing.timeseries_dataset_from_array(wav, wav, sequence_length=48000, sequence_stride=48000,batch_size=1)
    audio_slices = audio_slices.map(preprocess_mp3)
    audio_slices = audio_slices.batch(64)
    yhat = model.predict(audio_slices)
    results[file] = yhat

2、将预测转换为类

class_preds = {}
for file, logits in results.items():
    class_preds[file] = [1 if prediction > 0.99 else 0 for prediction in logits]

3、组连续检测

postprocessed = {}
for file, scores in class_preds.items():
    postprocessed[file] = tf.math.reduce_sum([key for key, group in groupby(scores)]).numpy()

十二、导出结果

import csv

with open('results.csv', 'w', newline='') as f:
    writer = csv.writer(f, delimiter=',')
    writer.writerow(['recording', 'capuchin_calls'])
    for key, value in postprocessed.items():
        writer.writerow([key, value])

它运行的结果是:

在这里插入图片描述

前面是音频文件,后面是该文件检测出多少卷尾鸟的叫声次数。

十三、最后我想说

这个项目在github上有源码,但是我克隆下来之后根本运行不了,经过我几天的努力终于解决了大部分的问题,但是最后有一个非常棘手的问题,困扰了我超级久,查阅了大量的帮助,还是不能解决,最后我不得不寻求大佬的帮助,请教了一位大佬,经过了长达两个小时的奋斗,最后终于成功了,真的是太不容易了。

在源码的基础上修改了很多,供大家学习,我也会继续研究一下这个代码的,值得去研究。

因为本期博客内容有点多,篇幅太长,后续我再更新期间遇见的问题以及我的解决办法,请大家耐心等待。

这次的创作时间特别的长,感觉自己的脑细胞要消耗完了,不过辛勤付出是值得的,我很开心,也特别期待得到大家的支持,谢谢!谢谢!

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-北天-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值