[Машинное обучение] Познакомьтесь со своим котом

искусственный интеллект

Соси кошек вместе с кодом! Эта статья участвует【Эссе "Мяу Звезды"】

Описание темы

Это название является заданием Ву Энда по программированию после школы, а также первой демонстрацией первого входа автора в машинное обучение.GitHub.com/AST и звезды научной…Найдите набор данных и полный код.

В наборе данных есть два вида изображений:

image-20211111135131667

image-20211111135213134

Один — кот, другой — не кот, и наша задача — научить классификатор классифицировать входную картинку, чтобы получить категорию картинки — кот или не кот.

Главная идея

Поскольку это новичок в машинном обучении, мы не используем здесь сверточные нейронные сети, а рассматриваем возможность использования для этого многослойного линейного персептрона (MLP), о чем мы часто говорим.полносвязный слой.

Весь процесс я разделил на следующие этапы:

  1. Чтение и обработка данных
  2. инициализация параметра
  3. прямое распространение
  4. Ошибка расчета
  5. обратное распространение
  6. обновить параметры
  7. предсказывать
  8. Реализация дополнительных функций

Чтобы понятнее понять различные процессы в MLP, я использую реализацию numpy.Конечно, методы реализации megengine и pytorch будут приведены в конце статьи.

Объяснение кода

Я считаю, что у каждого есть определенное понимание полной связи, функции активации, функции потерь, градиентного спуска и т. д., а затем подробно объяснить код.

Чтение и обработка данных

В обучающем наборе 209 изображений от 0.jpg до 208.jpg, в тестовом наборе 50 изображений и два файла txt, в которых хранится информация о категории изображения. Нам нужно прочитать и обработать эти данные. Код выглядит следующим образом:

def Read_label(path):  # 读取储存类别的文件
    with open(path, 'r') as file:
        # 去除返回字符串中的空格和换行符
        data = list(file.read().replace(' ', '').replace('\n', ''))
    label = list(map(int, data))  # 将列表元素转换为整型
​
    return label
​
​
def Read_data(path):  # 读取图片
    img = []
    filenames = os.listdir(path)
    filenames.sort(key=lambda x: int(x[:-4]))  # 将filenames排序,文件形式为(XX.jpg)
    for filename in filenames:
        img.append(cv2.resize(cv2.imread(
            path + filename, 1), (64, 64)))  # 以BGR形式读取图片
​
    return np.array(img)

Читать этикетку и картинку отдельно.Так как этикетка и картинка находятся во взаимно однозначном соответствии, используйтеos.listdirПри чтении он не будет следовать порядку размеров имен файлов, поэтому мы отсортируем имена файлов, используяcv2.imreadПрочтите, и, наконец, получите матрицу, которую мы будем использовать для обучения позже

Перед входом в сеть нам нужно обработать прочитанные данные — сгладить и нормализовать:

# 测试集和训练集和图片矩阵纵向维度保持一致
train_label = np.array(Read_label(Path_train_label)).reshape(1, -1)
test_label = np.array(Read_label(Path_test_label)).reshape(1, -1)
​
​
# 转置为(64*64*3, files acount)的矩阵(同一图片的矩阵信息转换到一列),并进行归一化
train_data = Read_data(Path_train).reshape(train_label.shape[1], -1).T/255
test_data = Read_data(Path_test).reshape(test_label.shape[1], -1).T/255
​

Здесь мы можем использовать другие методы нормализации, давайте рассмотрим их.

инициализация параметра

def Init_params(layers):  # 初始化权重矩阵和偏置
    # 好的参数初始化可使训练更快
    np.random.seed(3)  # 保证每次初始化一样
    parameters = {}  # 该字典用来储存参数
    L = len(layers)  # 神经网络的层数
​
    for l in range(1, L):
        parameters["W" + str(l)] = np.random.randn(layers[l],
                                                   layers[l - 1]) / np.sqrt(layers[l - 1])  # Xaiver初始化方法
        parameters['b'+str(l)] = np.zeros((layers_dims[l], 1))  # 初始化为0
​
    return parameters

Используйте метод инициализации Xaiver для инициализации, который может ускорить сходимость сети.Конечно, случайная инициализация также может использоваться, но будьте осторожны, чтобы не использовать всю инициализацию 0, чтобы обучение каждого уровня сети было согласованным, и теряется смысл многослойности. .

Следует отметить, что здесь используется словарь для хранения весов и смещений каждого слоя, а функция сохранения параметров будет добавлена ​​позже.

прямое распространение

def Forward_propagation(X, parameters):  # 向前传播
    """
    caches用于储存cache
    每一层的激活值A将输给下一层并作用于线性传播函数
    输出层的激活值为Yhat,将输给损失函数
    """
    caches = []
    A = X
    L = len(parameters) // 2  # 获得整型
    for l in range(1, L):  # (1,3)
        A, cache = Activation_forward(A, parameters['W' + str(l)],
                                      parameters['b' + str(l)], "Hiden")
        caches.append(cache)
    Yhat, cache = Activation_forward(A, parameters['W' + str(L)],
                                     parameters['b' + str(L)], "Output")
    caches.append(cache)
​
    return Yhat, caches

Параметры, хранящиеся здесь в кешах, будут использоваться для расчета градиента.

YhatПредставляет выходные данные последнего слоя, мы решим потери для этого результата и выполним обратное распространение

Мы добавляем функцию активации между каждыми двумя слоями, здесь я использовал TanH и функцию активации Sigmoid последнего слоя, конечно, вы также можете использовать известные вам функции активации, такие как ReLU и т. д.

def TanH(Z):
    return (np.exp(2*Z)-1)/(np.exp(2*Z)+1)
​
​
def Sigmoid(Z):
    return 1/(1+np.exp(-Z))

использоватьActivation_forwardЧтобы инкапсулировать процесс прямого распространения — нелинейная активация, содержащая линейное распространение и функцию активации, в то время как последний слой использует сигмовидную функцию активации.

def Activation_forward(A_pre, W, b, Type='Hiden'):  # 计算激活值
    """
    Z表示经过线性传播后的矩阵,将输给激活函数
    A_pre表示前一层的激活值,将输给线性传播单元,实现全连接性
    b将先广播至与W一样的大小,再进行运算
    """
    Z = Linear_forward(A_pre, W, b)
    cache = (A_pre, W, b)  # 储存参数用于反向传播
    # 若激活函数有ReLU函数,则需要将Z储存起来,供反向传播时使用
​
    if Type == "Output":
        A = Sigmoid(Z)
    elif Type == "Hiden":
        A = TanH(Z)
​
    return A, cache
​
def Linear_forward(A, W, b):  # 正向线性传播
    # 全连接通过权重矩阵实现,数据将由一层传递向下一层
    return np.dot(W, A) + b
​

Рассчитать потери

def Compute_cost(Yhat, Y):
    m = Y.shape[1]  # 图片张数
    # 交叉熵误差计算,与sigmoid函数复合成凸函数,凸函数以最低点为分界两边分别与Y的类别相对应
    cost = -np.sum(np.multiply(np.log(Yhat), Y) +
                   np.multiply(np.log(1 - Yhat), 1 - Y)) / m
    # 计算Yhat的梯度,由此开始反向传播
    dYhat = - (np.divide(Y, Yhat) - np.divide(1 - Y, 1 - Yhat))
​
    return cost, dYhat

Здесь в качестве функции потерь используется перекрестная энтропия, аlossотносительноYhatГрадиент , где начинается обратное распространение

обратное распространение

Нам нужно выполнить обратное распространение линейного слоя и двух функций активации, код выглядит следующим образом:

def Linear_backward(dZ, cache):
    A, W, b = cache  # 拆分cache
    m = A.shape[1]  # 获得图片张数
    # 除以m防止样本过大而导致数据过大
    dW = np.dot(dZ, A.T) / m  # dW/dZ=A.T,相乘代表与cost的梯度
    db = np.sum(dZ, axis=1, keepdims=True) / m  # db/dZ=I,保持维度不变
    dA = np.dot(W.T, dZ)
​
    return dA, dW, db
​
​
def Sigmoid_backward(dA, A):
    # Sigmoid函数导数为S(1-S)
    dZ = dA * A*(1-A)  # 相对于cost的梯度
​
    return dZ
​
​
def TanH_backward(dA, A):
    # TanH函数导数为1-H方
    dZ = dA*(1-A**2)  # 相对于cost的梯度
​
    return dZ

Следует отметить, что здесь используется цепное правило, поэтому полученный градиент напрямую связан с потерями, а не с входными данными слоя.

Затем инкапсулируйте приведенную выше функцию, чтобы представить один уровень обратного распространения:

def Activation_backward(dA, cache, A_next, activation="Hiden"):
    """
    cache储存A_pre,W,b
    A_next为输给下一层的激活值,即本层输出的激活值
    每次向后传播时都将前一层的计算得出的梯度输入,将直接得到该层参数与cost的梯度。
    """
    if activation == "Hiden":
        dZ = TanH_backward(dA, A_next)
    elif activation == "Output":
        dZ = Sigmoid_backward(dA, A_next)
    dA, dW, db = Linear_backward(dZ, cache)
​
    return dA, dW, db
def Backward_propagation(dYhat, Yhat, Y, caches):
    grads = {}  # 用于储存梯度矩阵
    L = len(caches)  # 4
    m = Y.shape[1]  # 图片个数
    # 输出层
    grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = Activation_backward(
        dYhat, caches[L-1], Yhat, "Output")
    # 隐藏层
    for l in reversed(range(L-1)):  # (3,0]
        grads["dA" + str(l + 1)], grads["dW" + str(l + 1)], grads["db" + str(l + 1)] = Activation_backward(
            grads["dA" + str(l + 2)], caches[l], caches[l+1][0], "Hiden")
        # caches[][0]储存的是A,此处意为A_next,即本层输出的激活值
​
    return grads

Здесь по-прежнему используется словарь для хранения градиентов, который будет использоваться для обновления параметров.

обновление параметра

def Update_params(parameters, grads, learning_rate):
    # 梯度下降更新参数
    L = len(parameters) // 2
    for l in range(L):
        parameters["W" + str(l + 1)] -= learning_rate * \
            grads["dW" + str(l + 1)]
        parameters["b" + str(l + 1)] -= learning_rate * \
            grads["db" + str(l + 1)]
​
    return parameters

Так просто, думаю, все поймут!

На данный момент вся сеть завершена, но не радуйтесь, нам еще нужно улучшить некоторые другие функции.

тренироваться

def Train_model(X, Y, parameters, learning_rate, iterations, threshold):  # 训练用模块
    costs = []  # 储存每100次迭代的损失值,用于绘制折线图
    for i in range(iterations):
        Yhat, caches = Forward_propagation(X, parameters)  # 正向传播
        cost, dYhat = Compute_cost(Yhat, Y)  # 计算误差
        grads = Backward_propagation(dYhat, Yhat, Y, caches)  # 计算梯度
        parameters = Update_params(
            parameters, grads, learning_rate)  # 更新参数
        if i % 100 == 0:
            costs.append(cost)
            print(f"迭代次数:{i},误差值:{cost}")
        if cost < threshold:  # 通过损失值
            costs.append(cost)
            print(f"迭代次数:{i},误差值:{cost}")
            break
​
    return parameters, costs, i

X и Y представлены в виде изображений и меток соответственно, параметры — это параметры сети, learning_rate — скорость обучения, итерации — количество итераций, а порог можно использовать для предварительного завершения обучения через значение потерь.

Возвращаемые здесь параметры будут использоваться для сохранения параметров, а затраты будут использоваться для построения кривой потерь.

построить кривую потерь

def Plot(costs, layers):
    plt.plot(costs)
    plt.ylabel('cost')
    plt.xlabel('iterations')
    plt.title("Learning rate =" +
              str(learning_rate) + f",layers={layers}")
    plt.show()
​

При рисовании кривой будет добавлена ​​некоторая информация о конфигурации сети.

Сохранение и чтение параметров

def Save_params(parameters, layers, path):
    # 储存神经网络各层的信息
    np.savetxt(path+'layers.csv', layers, delimiter=',')
    n = len(parameters)//2
    # 将每个参数分开储存,方便读取
    for i in range(1, n+1):
        np.savetxt(path+'W'+str(i)+'.csv',
                   parameters['W'+str(i)], delimiter=',')
        np.savetxt(path+'b'+str(i)+'.csv',
                   parameters['b'+str(i)], delimiter=',')
​
​
def Load_params(path):
    parameters = {}  # 用于接收参数
    layers = list(np.loadtxt(path+'layers.csv', dtype=int, delimiter=','))
    n = len(layers)
    for i in range(1, n):
        parameters['W'+str(i)] = np.loadtxt(path+'W'+str(i) +
                                            '.csv', delimiter=",").reshape(layers[i], -1)
        parameters['b'+str(i)] = np.loadtxt(path+'b'+str(i) +
                                            '.csv', delimiter=",").reshape(layers[i], 1)
​
    return layers, parameters

использоватьnp.savetxtДля хранения сохраните веса и смещения каждого слоя в указанной папке и используйте унифицированный метод именования для облегчения сохранения и чтения.В качестве примера для четырехслойной сети вы можете получить следующее:

image-20211111145254570

Визуальный интерфейс

После нескольких минут обучения мы получаем обученный файл параметров, который мы можем загрузить, чтобы делать прогнозы, но как же нам не иметь «красивого» интерфейса? Здесь я использую пакет tkinter для реализации простого визуального интерфейса.

def Create_window(parameters):
    global window  # global便于后续引用
    window = tk.Tk()
    window.title('猫咪识别器')
    window.geometry('650x650')
    window.configure(background='lightpink')
    label = tk.Label(window, text='每次识别后等待5秒即可再次识别!',
                     font=('楷书', 15), fg='Purple', bg='orange')
    label.pack(side='top')
    num = tk.Label(window, text='2000301712', font=(
        'fira_Code'), bg='orange', fg='purple') #修改颜色
    num.pack(side='right')
    # lambda可以防止带参数的函数自动运行
    choose_button = tk.Button(window, text='打开一张图片', fg='deeppink', bg='violet', activebackground='yellow',
                              font=('宋体', 20), command=lambda: Show_img(parameters))
    choose_button.pack(side='bottom')
    window.mainloop()
​
​
def Show_img(parameters):
    global window, img
    file = tk.filedialog.askopenfilename()  # 获取选择的文件路径
    Img = Image.open(file)
    # 使用cv2读取图片,供后续预测使用(cv2读取图片通道顺序为BGR)
    data = cv2.resize(cv2.imread(file, 1), (64, 64)).reshape(1, -1).T/255
    img = ImageTk.PhotoImage(Img)
    Predict_button = tk.Button(window, text='识别!', fg='CornflowerBlue', bg='slateblue', activebackground='red',
                               font=('宋体', 20), command=lambda: Predict(data, parameters))
    Predict_button.pack(side='bottom')
    Predict_button.after(5000, Predict_button.destroy)  # 一段时间后销毁按钮
    label_Img = tk.Label(window, image=img)  # 显示图片
    label_Img.pack(side='top')
    label_Img.after(5000, label_Img.destroy)
​

После запуска вы можете получить следующий «мужской» интерфейс (работа на Windows может нормально отображать китайский язык): \

image-20211111151210599

Если вы не думаете, что это выглядит хорошо, вы можете изменить его в приведенном выше коде.

Нажмите кнопку ниже, чтобы выбрать изображение для предсказания:

image-20211111151424140

Смайлик ниже означает, что это кошка, и в командной строке также будет указана вероятность того, что это кошка.

image-20211111151501429

полный код

Полный код и набор данных можно получить, посетив мой github:GitHub.com/AST и звезды научной…

MegEngine

МегЭнджин:GitHub.com/Мег двигатель/М…Здесь реализована только часть модели:

import megengine as mge
import megengine.module as M
​
class CustomMLP(M.Module):
    def __init__(self, layers:list, in_dim:int):
        super(CustomMLP, self).__init__() 
        self.modules = M.Sequential(*self._make_layer(layers, in_dim))
​
    def forward(self,inputs):
        for moudule in self.modules:
            inputs = moudule(inputs)
        return inputs
​
    def _make_layer(self, layers, in_dim):
        length = len(layers)
        modules = [M.Linear(in_dim, layers[0])]
        for i in range(length-1):
            activation = M.ReLU()
            layer = M.Linear(layers[i], layers[i+1])
            modules.append(activation)
            modules.append(layer)
        modules.append(M.Sigmoid())
        return modules
​
model = CustomMLP([20,8,7,1],3)
print(model)

Запустив можно получить структурную схему сети:

image-20211111153725217

Pytorch

import torch
import torch.nn as nn
​
class CustomMLP(nn.Module):
    def __init__(self, layers:list, in_dim:int):
        super(CustomMLP, self).__init__() 
        self.modules = nn.Sequential(*self._make_layer(layers, in_dim))
​
    def forward(self,inputs):
        for moudule in self.modules:
            inputs = moudule(inputs)
        return inputs
​
    def _make_layer(self, layers, in_dim):
        length = len(layers)
        modules = [nn.Linear(in_dim, layers[0])]
        for i in range(length-1):
            activation = nn.ReLU()
            layer = nn.Linear(layers[i], layers[i+1])
            modules.append(activation)
            modules.append(layer)
        modules.append(nn.Sigmoid())
        return modules
    
model = CustomMLP([20,8,7,1], 3)
print(model)

Эти два кода очень похожи. Если у вас есть какие-либо вопросы, пожалуйста, свяжитесь со мной, чтобы ответить!