一,Torch基础及其线性回归实战
"""
此部分是关于torch张量知识的总结
1,覆盖了torch框架如何创建一个tensor,以及对tensor进行切片操作
2,介绍了gpu相关api,从而加速矩阵计算
"""
import torch
import matplotlib.pyplot as plt
if False:
# 定义一个tensor(张量) 5行三列,数值随即
x = torch.rand(5, 3)
print(x)
# 0行0列
print(x[0, 0])
# 最后一列全部元素
print(x[:, -1])
# 第一行,1到最后一列元素
print(x[0, 1:])
# 定义一个举证,5x3,初始值全为1
print(torch.ones(5, 3))
# tensor([[1., 1., 1.],
# [1., 1., 1.],
# [1., 1., 1.],
# [1., 1., 1.],
# [1., 1., 1.]])
# 同尺寸矩阵操作,相加
print(torch.ones(5, 3) + x)
# 矩阵相惩 [5,3] * [3,5] ---》 [5,5] tensor.t()是对矩阵转置
print(x.mm(torch.ones(5, 3).t()))
# torch 张量与numpy相似,其重复轮子的优势在于,torch能在GPU上计算,而numpy不能
# 输出是否能使用gpu
print(torch.cuda.is_available())
if torch.cuda.is_available():
# x.cuda() 即将张量放置到gpu上,进而可以使用gpu计算
print(x.cuda().mm(torch.ones(5, 3).cuda().t()))
# 此命令是将数据从gpu卸载到cpu
x.cuda().cpu()
"""
神经网络 反向传播算法 更新内部神经元,提高训练效率
目前大多数深度学习采用了 计算图 技术,计算图是一个有向图,方形节点是变量,圆形节点表示运算
核心思想:记录正向计算步骤,只要此步骤可微分(求导),这自动计算每个变量的梯度
"""
# PyTorch借助自动微分变量实现了 动态计算图, PyTorch会保存运算路径,从而通过backword进行反向传播
if False:
# requires_grad=True,保证可以在执行反向传播算法的过程中,获得梯度信息
x = torch.ones(2, 2, requires_grad=True)
print(x)
y = x + 2
print(y)
print(y.grad_fn)
# y.grad_fn存储了运算信息,AddBackward0,它是计算图上一个运算节点
# tensor([[3., 3.],
# [3., 3.]], grad_fn= < AddBackward0 >)
# 从而得到一个计算图如下
# [x]<---(+2)<---[y]
z = y * y
print(z)
print(z.grad_fn)
# tensor([[9., 9.],
# [9., 9.]], grad_fn= < MulBackward0 >)
# 计算图如下
# [x]<---(+2)<---[y]
# ↑
# |
# |
# (*) <----[z]
# 对z求平均,即矩阵元素求和后除总数
t = torch.mean(z)
print(t)
# .data存储计算结果
print(t.data)
# tensor(9., grad_fn=<MeanBackward0>)
# 计算图如下
# [x]<---(+2)<---[y]
# ↑
# |
# |
# (*) <----[z]
# ↑
# |
# (mean)<---[t]
"""
进一步求导,PyTorch通过backward方法求导并且自动运行反向传播算法
x.grad保存了梯度信息,即求导,
只有叶节点才保存有grad信息,因此以下计算只有x存在值
"""
t.backward()
print(z.grad)
# None
print(y.grad)
# None
print(x.grad)
# dt/dx值如下
# tensor([[1.5000, 1.5000],
# [1.5000, 1.5000]])
# 进一步理解backward实例
s = torch.tensor([[0.01, 0.02]], requires_grad=True)
x = torch.ones(2, 2, requires_grad=True)
for i in range(10):
s = s.mm(x)
z = torch.min(s)
z.backward()
print(x.grad)
# dz/dx
# tensor([[37.1200, 37.1200],
# [39.6800, 39.6800]])
# s非叶节点了
print(s.grad)
# None
"""
实战一:预测房价
本篇,将通过神经网络算法,对房价进行线性回归预测
"""
if True:
# 时间变量
# 生成100个 0~100等距数
x = torch.linspace(0, 100, 100).type(torch.FloatTensor)
# randn 正态分布,均值为0,方差为10
rand = torch.randn(100) * 10
y = rand + x
x_train = x[: -10]
x_test = x[-10:]
y_train = y[:-10]
y_test = y[-10:]
# 设定窗口大小为10x8
plt.figure(figsize=(10, 8))
plt.xlabel("X")
plt.ylabel("Y")
# plt.plot(x_train.data.numpy(), y_train.data.numpy(), 'o')
# plt.show()
# 求解拟合函数 y = ax + b
# L(a,b)梯度优化,
a = torch.rand(1, requires_grad=True)
b = torch.rand(1, requires_grad=True)
learning_rate = 0.0001
# 迭代学习1000次
for i in range(1000):
# y = ax + b, expand_as是扩大ab尺寸与训练集匹配
predictions = a.expand_as(x_train) * x_train + b.expand_as(x_train)
# 定义损失函数,
# L = mean((y1-y2)^2) ,mean = 求均值,即 L = 1/N * ∑(y - yi)^2 ,可以理解为实际值与预测值差值求和
loss = torch.mean((predictions - y_train) ** 2)
print(f"loss:{loss}")
# 对损失函数进行梯度反转
# 这一步很关键,在于能够计算出dy/dx,并且根据梯度,调整ab参数,使得更加拟合训练集,
# backward,反向传播算法,神经网络高效训练的核心,
loss.backward()
# 利用上一步计算结果,更新ab的data值
# add_表示需要变化值当前变量,没有_则是返回新值
a.data.add_(-learning_rate * a.grad.data)
b.data.add_(-learning_rate * b.grad.data)
# 清空ab梯度值
a.grad.data.zero_()
b.grad.data.zero_()
print(f"a = {a},b = {b}")
# 绘制拟合曲线
# 训练集xy分布图
xplot = plt.plot(x_train.data.numpy(), y_train.data.numpy(), 'o')
# 拟合线 y = ax+b
yplot = plt.plot(x_train.data.numpy(), a.data.numpy() * x_train.data.numpy() + b.data.numpy())
plt.xlabel("X")
plt.ylabel("Y")
plt.legend([xplot, yplot], ['Data', str(a.data.numpy()[0]) + 'x +' + str(b.data.numpy()[0])])
# plt.show()
# 根据已经训练的模型,进行预测
predictions = a.expand_as(x_test) * x_test + b.expand_as(x_test)
# 绘制测试数据
plt.plot(x_test.data.numpy(), y_test.data.numpy(), 's')
# 绘制预测曲线
plt.plot(x_test.data.numpy(), predictions.data.numpy(), 's')
plt.show()
print(predictions)
二,神经网络定义实战
"""
此部分通过实例进一步学习神经网络,
Question:存在一年数据,三个变量(时间t月日,地点w,单车数量c)共享单车某时刻t在某地的数量c,
通过构建一个简单的神经网络,预测未来该地共享单车数量,以解决共享单车投放问题
本篇中,将会学到什么是神经网络,如何构建一个神经网络,什么是过拟合,怎么解决过拟合,以及激活函数、机器学习等基本概念。
"""
import torch.nn
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
"""
神经网络,可以说是任何函数的通用拟合器,即通过多项式分解,可以映射到任意函数,而这些多项式的参数,即神经网络概念
输入 -> [神经网络(输入层、隐含层、输出层)] -> 特征值输出
假定一个全连接层神经网络如下
[w11 w12 .... w1n]
[ ]
[ ]
输入->特征向量 * [ ] -> 输出特征向量
[ ]
[ ]
[ ]
[wm1 wm2 .... wmn]
输入层 隐含层 输出层
神经网络的学习,即获得一组矩阵参数,与输入矩阵做矩阵相乘,即可得到输出矩阵,来实现输入->输出的预测
除了以上全连接型(每一列与下一列是一一连接的),还有卷积神经网络(通过卷积提取特征值,在通信、图像、信号领域)
那么如何得到一组较好的w参数呢,便是训练,即通过输入、预测结果y、实际结果yi,
下一步,即定义一个损失函数L,使得预测值和实际值差值变小,比如 L = 1/N * ∑(y - yi)^2
通过对dL/dx求导,进一步调整w参数,就是不断拟合、即梯度下降的过程
Torch能对损失函数L.backward即反向传播算法,得到一个dL/dx,保存了grad,通过此参数调整w
"""
# # 因为没有训练集,这里以MNIST数据集为例,
# # 用途:手写数字分类(0-9,共 10 类)。
# # 样本数量:60,000 个训练样本,10,000 个测试样本。
# # 数据类型:灰度图像,大小为 28x28。
train_dataset = MNIST(root='data', train=True, download=True, transform=ToTensor())
test_dataset = MNIST(root='data', train=False, download=True, transform=ToTensor())
input_size = 28 * 28 # 图片平铺为一维向量
hidden_size = 128 # 隐藏层
output_size = 10 # 输出层,0~9
batch_size = 128
# 定义一个神经网络,Sequential传入神经网络层,
neu = torch.nn.Sequential(
# 平铺向量,将28x28二维图平铺为1维向量
torch.nn.Flatten(),
# 输入层
torch.nn.Linear(input_size, hidden_size),
# 激活函数,是神经网络重要组成部分,因其目的,可以选择激活函数,
# ReLU(x) = max(0,x)
torch.nn.ReLU(),
# 输出范围: 映射到区间 (0, 1)。
# 单调性: 是单调递增的函数。
# 平滑性: 输出是连续且平滑的,适合用于需要概率输出的场景。
# 饱和性: 当输入值 (x) 过大或过小时,输出值会趋近于 1 或 0,可能导致梯度消失问题。
# torch.nn.Sigmoid(),
# 输出层
torch.nn.Linear(hidden_size, output_size)
)
# 使用torch自带的损失函数
# loss = torch.nn.MSELoss()
# 交叉熵损失函数
loss_criterion = torch.nn.CrossEntropyLoss()
# r = loss(预测向量,目标向量)
# SDG是torch自带的随即梯度下降算法,lr是学习率
optimizer = torch.optim.SGD(neu.parameters(), lr=0.01)
# 是否加载GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
neu.to(device)
# 使用 DataLoader 加载数据
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 标记模型进入train状态
neu.train()
# 记录损失函数值下降过程
batch_loss = []
for i in range(3):
for batch_idx, (data, targets) in enumerate(train_loader):
# data, targets = data.to(device), targets.to(device)
# 前向传播
outputs = neu(data)
# 根据目标值计算损失值
loss = loss_criterion(outputs, targets)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
batch_loss.append(loss.item())
print(loss)
# 测试模型
# 标记模型进入test状态
neu.eval()
correct = 0
total = 0
with torch.no_grad():
for data, targets in test_loader:
# 预测输出
outputs = neu(data)
# 获取预测结果,取输出向量最大一个元素
_, predicted = torch.max(outputs, 1)
total += targets.size(0)
correct += (predicted == targets).sum().item()
print(f"Accuracy:{100 * correct / total}%")
三,卷积神经网络
"""
通过卷积神经网络完成MNIST数据集训练
卷积c1->池化->卷积c2->池化->特征->特征
什么是卷积?它是对信号做积分运算。
在深度学习中,卷积操作主要用于提取图像的局部特征。
它的核心思想是:
使用一个小的卷积核(滤波器)在输入图像上滑动。
通过加权和运算提取出图像的局部特征,例如边缘、纹理、角点等。
什么是池化?
池化(Pooling)是卷积神经网络(CNN)中常用的操作,主要用于对特征图进行降维和特征提取。
池化操作的核心思想是:
缩小特征图的尺寸,从而减少计算量和内存需求。
保留主要特征,增强模型的鲁棒性(如抵抗旋转、缩放和位移等变化)。
防止过拟合,通过减少特征图的参数和冗余信息,提高模型的泛化能力。
这相当于俯瞰正片森林而不拘于一棵树,从宏观上观察事物,提高泛化能力。
"""
if True:
import torch.nn.functional as F
# 使用 DataLoader 加载数据
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 定义两个卷积层厚度
depth = [4, 8]
# 28x28
image_size = 28
num_classes = 10
class ConvNet(torch.nn.Module):
def __init__(self):
# 父类构造方法
super(ConvNet, self).__init__()
# 定义一个卷积核
self.conv1 = torch.nn.Conv2d(1, 4, 5, padding=2)
# 定义一个池化层 2x2
self.pool = torch.nn.MaxPool2d(2, 2)
# 定义第二个卷积核
self.conv2 = torch.nn.Conv2d(depth[0], depth[1], 5, padding=2)
# 定义一个线性全连接层,即特征输入层
self.fc1 = torch.nn.Linear(image_size // 4 * image_size // 4 * depth[1], 512)
# 定义一个输出层
self.fc2 = torch.nn.Linear(512, num_classes)
def forward(self, x):
"""
模板方法,表示根据输入x进行神经网络前向运算
:param x: 输入张量
:return: x
"""
# 正向通过第一个卷积层
x = self.conv1(x)
# 激活函数,防止过拟合
x = F.relu(x)
# 池化
x = self.pool(x)
# 第二个卷积层
x = self.conv2(x)
x = F.relu(x)
# 第四层池化,将窗口缩小到1/4
x = self.pool(x)
# 让x特征值排布向量,以进入第一个全连接层
x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])
# 全连接层
x = F.relu(self.fc1(x))
# 默认0.5概率对此层进行dropout操作,防止过拟合
# 即根据概率,训练中选择丢弃一些神经,最后的测试再使用全部神经,以提高模型泛华能力
# 就好比一组学生,平时总有几个人不在,于是每个人承担的任务更多,最后考试的时候集体成绩却更好
x = F.dropout(x)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x
def retrieve_features(self, x):
"""
用户提取卷积神经网络的特征图
:param x:
:return: feature_map1、feature_map2,卷积神经网络前两层特征图
"""
feature_map1 = F.relu(self.conv1(x))
x = self.pool(feature_map1)
feature_map2 = F.relu(self.conv2(x))
return feature_map1, feature_map2
# 运行模式
model = ConvNet()
# 损失函数:交叉熵
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# 标记模型train,这样会打开dropout
model.train()
for i in range(5):
for batch_idx, (data, targets) in enumerate(train_loader):
data, targets = data.clone().requires_grad_(True), targets.clone().detach() # detach从计算图中分离,不会计算其梯度
# 前向传播
outputs = model(data)
loss = criterion(outputs, targets)
optimizer.zero_grad()
loss.backward()
# 一步随即梯度下降算法
optimizer.step()
print(loss)
# 测试集测试
# 关闭dropout
model.eval()
correct = 0
total = 0
with torch.no_grad():
for data, targets in test_loader:
# 预测输出
outputs = model(data)
# 获取预测结果,取输出向量最大一个元素
_, predicted = torch.max(outputs, 1)
total += targets.size(0)
correct += (predicted == targets).sum().item()
print(f"Accuracy:{100 * correct / total}%")
可以看到,准确率比全连接高了5%,毕竟卷积善于对图像处理。
四,迁移学习
"""
此部分通过实例进一步学习 迁移学习,
所谓迁移学习,即通过已经训练好的良好泛华能力的模型,加上特定的特征层,即可实现垂直领域功能,
本文,通过对一个ResNet模型进行迁移学习,使得能识别蜜蜂。ResNet是一个上百层的卷积网络,在图像识别领域已有很高泛化能力
"""
import torch
import torch.nn as nn
from torchvision import datasets, models, transforms
# 原始图像224x224
image_size = 224
train_dataset = datasets.ImageFolder("data/train", transforms.Compose([transforms.RandomResizedCrop(image_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])]))
val_dataset = datasets.ImageFolder("data/val", transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])]))
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=4)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=4, shuffle=True, num_workers=4)
# 读取数据类别
num_classes = len(train_dataset.classes)
# 调用已经训练好的model,比如此处的ResNet
origin_model = models.resnet18(pretrained=True)
# 禁止原模型层被微分,即不改变原模型参数
for params in origin_model.parameters():
params.requires_grad = False
# 接下来,使用预训练迁移网络
# num_ftrs存储了res18模型最后的全连接层
num_ftrs = origin_model.fc.in_features
# 将原model的最后两层全连接层替换为一个输出单位为2的全连接层,此处任务是识别是或否蜜蜂
origin_model.fc = nn.Linear(num_ftrs, 2)
# 定义损失函数、优化器
loss_criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(origin_model.parameters(), lr=0.0001, momentum=0.9)
# do train
for i in range(3):
for batch_idx, (data, targets) in enumerate(train_loader):
data, targets = data.clone().requires_grad_(True), targets.clone().detach()
# 前向传播
outputs = origin_model(data)
# 根据目标值计算损失值
loss = loss_criterion(outputs, targets)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(loss)
五,神经网络风格迁移实战
所谓风格迁移,即将一幅画的风格迁移到另一副画
准备:
对应目录300*300png图,以及vgg19模型参数
"""
本章,尝试使用torch,使用已有模型,进行风格迁移学习,
Question:参考一幅图,通过像素的梯度下降(不改变参数,只改变目标图像素),以实现风格迁移
"""
from __future__ import print_function
import torch
import torch.nn as nn
from PIL import Image
import matplotlib.pyplot as plt
import torchvision.transforms as transform
import torchvision.models as models
import copy
style = "./image_test/style2.png"
content = "./image_test/content.png"
style_weight = 1000
content_weight = 1
# 生成图片size
imsize = 300
loader = transform.Compose([transform.Resize(imsize), transform.ToTensor()])
unloader = transform.ToPILImage()
# 加载图片
def image_loader(image_path):
image = Image.open(image_path)
image = loader(image).clone().detach().requires_grad_(True)
# 虚拟一个batch维度
return image.unsqueeze(0)
style_img = image_loader(style).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor)
content_img = image_loader(content).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor)
assert style_img.size() == content_img.size()
def show_image(tensor, title=None):
image = tensor.clone().cpu()
image = image.view(3, imsize, imsize)
image = unloader(image)
plt.imshow(image)
plt.title(title)
plt.pause(0.001)
plt.ion()
plt.figure()
show_image(style_img.data, title="风格图")
plt.figure()
show_image(content_img.data, title="目标图")
"""
加载VGC模型,该模型是视觉Model
"""
vgg19_model = models.vgg19()
vgg19_model.load_state_dict(torch.load("models_local/vgg19-dcbb9e9d.pth"))
cnn = vgg19_model.features
if torch.cuda.is_available():
cnn = cnn.cuda()
def Gram(i):
# a=batch (size=1),b是特征图 (c,d)是特征图尺寸
a, b, c, d = i.size()
features = i.view(a * b, c * d)
G = torch.mm(features, features.t())
# 通过除以特征图中的像素数量来将特征图归一
return G.div(a * b * c * d)
class ContentLoss(nn.Module):
def __init__(self, target, weight):
super(ContentLoss, self).__init__()
self.output = None
self.loss = None
self.target = target.detach() * weight
self.weight = weight
# 定义损失函数
self.criterion = nn.MSELoss()
def forward(self, i):
self.loss = self.criterion(i * self.weight, self.target)
self.output = i
return self.output
def backward(self, retain_graph=True):
self.loss.backward(retain_graph=retain_graph)
return self.loss
class StyleLoss(nn.Module):
def __init__(self, target, weight):
super(StyleLoss, self).__init__()
self.output = None
self.loss = None
self.target = target.detach() * weight
self.weight = weight
# 定义损失函数
self.criterion = nn.MSELoss()
# self.gra, = GramMatrix()
def forward(self, i):
self.output = i.clone()
i = i.cuda() if torch.cuda.is_available() else i
self_G = Gram(i)
self_G.mul_(self.weight)
self.loss = self.criterion(self_G, self.target)
return self.output
def backward(self, retain_graph=True):
self.loss.backward(retain_graph=retain_graph)
return self.loss
content_layers = ['conv_4']
style_layers = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']
"""
start style train
"""
model = nn.Sequential()
style_losses = []
content_losses = []
i = 1
for layer in list(cnn):
# 遍历vgc神经网络层,找到卷积网络
if isinstance(layer, nn.Conv2d):
name = "conv_" + str(i)
model.add_module(name, layer)
if name in content_layers:
# 如果位于内容计算层
target = model(content_img).clone()
content_loss = ContentLoss(target, content_weight)
model.add_module(f"content_loss_{i}", content_loss)
# 输出损失值
print(f"content_loss_{i}:{content_loss}")
content_losses.append(content_loss)
if name in style_layers:
target_feature = model(style_img).clone()
target_feature_gram = Gram(target_feature)
style_loss = StyleLoss(target_feature_gram, style_weight)
model.add_module(f"style_loss_{i}", style_loss)
print(f"style_loss_{i}:{style_loss}")
style_losses.append(style_loss)
if isinstance(layer, nn.ReLU):
name = f"relu_{i}"
model.add_module(name, layer)
i += 1
if isinstance(layer, nn.MaxPool2d):
model.add_module(f"pool_{i}", layer)
print(model)
# 生成一张随即噪声图
input_img = torch.randn(content_img.data.size()).requires_grad_(True)
show_image(input_img.data, title="input image")
# 将输入图作为参数调整对象,即调整像素本身
input_params = nn.Parameter(input_img.data)
# 使用LBFGS算法优化器,
optimizer = torch.optim.LBFGS([input_params])
# 定义优化300轮
for i in range(300):
# 限制输入图像色彩0~1
input_params.data.clamp_(0, 1)
# 清空梯度
optimizer.zero_grad()
# 放入神经网络
model(input_params)
style_score = 0
content_score = 0
for sl in style_losses:
style_score += sl.backward()
for cl in content_losses:
content_score += cl.backward()
print(f"运行{i}轮\n风格损失Content={content_score},Style={style_score}")
optimizer.step(closure=lambda: content_score + style_score)
# 防止越界
output = input_params.data.clamp_(0, 1)
plt.figure()
show_image(output, title="fix image")
plt.ioff()
plt.show()