文章目录

  • 第3章 线性分类
    • 3.1 基于Logistic回归的二分类任务
      • 3.1.1 数据集构建
      • 3.1.2 模型构建
      • 3.1.3 损失函数
      • 3.1.4 模型优化
        • 3.1.4.1 梯度计算
        • 3.1.4.2 参数更新
      • 3.1.5 评价指标
      • 3.1.6 完善Runner类
      • 3.1.7 模型训练
      • 3.1.8 模型评价
    • 3.2 基于Softmax回归的多分类任务
      • 3.2.1 数据集构建
      • 3.2.2 模型构建
        • 3.2.2.1 Softmax函数
        • 3.2.2.2 Softmax回归算子
      • 3.2.3 损失函数
      • 3.2.4 模型优化
        • 3.2.4.1 梯度计算
        • 3.2.4.2 参数更新
      • 3.2.5 模型训练
      • 3.2.6 模型评价
    • 3.3 实践:基于Softmax回归完成鸢尾花分类任务
      • 3.3.1 数据处理
        • 3.3.1.1 数据集介绍
        • 3.3.1.2 数据清洗
        • 3.3.1.3 数据读取
      • 3.3.2 模型构建
      • 3.3.3 模型训练
      • 3.3.4 模型评价
      • 3.3.5 模型预测
    • 3.4 小结
    • 3.5 实验拓展

pytorch实现

第3章 线性分类

3.1 基于Logistic回归的二分类任务

3.1.1 数据集构建

我们首先构建一个简单的分类任务,并构建训练集、验证集和测试集。
本任务的数据来自带噪音的两个弯月形状函数,每个弯月对一个类别。我们采集1000条样本,每个样本包含2个特征。

数据集的构建函数make_moons的代码实现如下:

import math
import copy
import torchdef make_moons(n_samples=1000, shuffle=True, noise=None):"""生成带噪音的弯月形状数据输入:- n_samples:数据量大小,数据类型为int- shuffle:是否打乱数据,数据类型为bool- noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声输出:- X:特征数据,shape=[n_samples,2]- y:标签数据, shape=[n_samples]"""n_samples_out = n_samples // 2n_samples_in = n_samples - n_samples_out# 采集第1类数据,特征为(x,y)# 使用'torch.linspace'在0到pi上均匀取n_samples_out个值# 使用'torch.cos'计算上述取值的余弦值作为特征1,使用'torch.sin'计算上述取值的正弦值作为特征2outer_circ_x = torch.cos(torch.linspace(0, math.pi, n_samples_out))outer_circ_y = torch.sin(torch.linspace(0, math.pi, n_samples_out))inner_circ_x = 1 - torch.cos(torch.linspace(0, math.pi, n_samples_in))inner_circ_y = 0.5 - torch.sin(torch.linspace(0, math.pi, n_samples_in))print('outer_circ_x.shape:', outer_circ_x.shape, 'outer_circ_y.shape:', outer_circ_y.shape)print('inner_circ_x.shape:', inner_circ_x.shape, 'inner_circ_y.shape:', inner_circ_y.shape)# 使用'torch.cat'将两类数据的特征1和特征2分别延维度0拼接在一起,得到全部特征1和特征2# 使用'torch.stack'将两类特征延维度1堆叠在一起X = torch.stack([torch.cat([outer_circ_x, inner_circ_x]),torch.cat([outer_circ_y, inner_circ_y])],axis=1)print('after concat shape:', torch.cat([outer_circ_x, inner_circ_x]).shape)print('X shape:', X.shape)# 使用'torch. zeros'将第一类数据的标签全部设置为0# 使用'torch. ones'将第一类数据的标签全部设置为1y = torch.cat([torch.zeros([n_samples_out]), torch.ones([n_samples_in])])print('y shape:', y.shape)# 如果shuffle为True,将所有数据打乱if shuffle:# 使用'torch.randperm'生成一个数值在0到X.shape[0],随机排列的一维Tensor做索引值,用于打乱数据idx = torch.randperm(X.shape[0])X = X[idx]y = y[idx]# 如果noise不为None,则给特征值加入噪声if noise is not None:# 使用'torch.normal'生成符合正态分布的随机Tensor作为噪声,并加到原始特征上X += torch.normal(0, noise, X.shape)return X, y

随机采集1000个样本,并进行可视化。

# 采样1000个样本
n_samples = 1000
X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.5)
# 可视化生产的数据集,不同颜色代表不同类别
import matplotlib.pyplot as pltplt.figure(figsize=(5,5))
plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())
plt.xlim(-3,4)
plt.ylim(-3,4)
plt.savefig('linear-dataset-vis.pdf')
plt.show()

结果:


将1000条样本数据拆分成训练集、验证集和测试集,其中训练集640条、验证集160条、测试集200条。代码实现如下:

num_train = 640
num_dev = 160
num_test = 200X_train, y_train = X[:num_train], y[:num_train]
X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]y_train = y_train.reshape([-1,1])
y_dev = y_dev.reshape([-1,1])
y_test = y_test.reshape([-1,1])

这样,我们就完成了Moon1000数据集的构建。

# 打印X_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)
# 打印一下前5个数据的标签
print (y_train[:5])

3.1.2 模型构建

Logistic回归是一种常用的处理二分类问题的线性模型。与线性回归一样,Logistic回归也会将输入特征与权重做线性叠加。不同之处在于,Logistic回归引入了非线性函数g:R→(0,1),预测类别标签的后验概率 p(y=1|x) ,从而解决连续的线性函数不适合进行分类的问题。

其中激活函数为Logistic函数,作用是将线性函数f(x;w,b)的输出从实数区间“挤压”到(0,1)之间,用来表示概率。Logistic函数定义为:


Logistic函数的代码实现如下:

# 定义Logistic函数
def logistic(x):return 1 / (1 + torch.exp(-x))# 在[-10,10]的范围内生成一系列的输入值,用于绘制函数曲线
x = torch.linspace(-10, 10, 10000)
plt.figure()
plt.plot(x.tolist(), logistic(x).tolist(), color="#e4007f", label="Logistic Function")
# 设置坐标轴
ax = plt.gca()
# 取消右侧和上侧坐标轴
ax.spines['top'].set_color('none')
ax.spines['right'].set_color('none')
# 设置默认的x轴和y轴方向
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
# 设置坐标原点为(0,0)
ax.spines['left'].set_position(('data',0))
ax.spines['bottom'].set_position(('data',0))
# 添加图例
plt.legend()
plt.savefig('linear-logistic.pdf')
plt.show()

从输出结果看,当输入在0附近时,Logistic函数近似为线性函数;而当输入值非常大或非常小时,函数会对输入进行抑制。输入越小,则越接近0;输入越大,则越接近1。正因为Logistic函数具有这样的性质,使得其输出可以直接看作为概率分布。

Logistic回归算子

Logistic回归模型其实就是线性层与Logistic函数的组合,通常会将 Logistic回归模型中的权重和偏置初始化为0,同时,为了提高预测样本的效率,我们将N个样本归为一组进行成批地预测。

其中X∈RN×D为N个样本的特征矩阵,y^为N个样本的预测值构成的N维向量。
这里,我们构建一个Logistic回归算子,代码实现如下:

# 线性算子
class Op(object):def __init__(self):passdef __call__(self, inputs):return self.forward(inputs)def forward(self, inputs):raise NotImplementedErrordef backward(self, inputs):raise NotImplementedError
op=Opclass model_LR(op):def __init__(self, input_dim):super(model_LR, self).__init__()self.params = {}# 将线性层的权重参数全部初始化为0self.params['w'] = torch.zeros([input_dim, 1])# self.params['w'] = torch.normal(mean=0, std=0.01, shape=[input_dim, 1])# 将线性层的偏置参数初始化为0self.params['b'] = torch.zeros([1])def __call__(self, inputs):return self.forward(inputs)def forward(self, inputs):"""输入:- inputs: shape=[N,D], N是样本数量,D为特征维度输出:- outputs:预测标签为1的概率,shape=[N,1]"""# 线性计算score = torch.matmul(inputs, self.params['w']) + self.params['b']# Logistic 函数outputs = logistic(score)return outputs

测试一下
随机生成3条长度为4的数据输入Logistic回归模型,观察输出结果。

# 固定随机种子,保持每次运行结果一致
torch.seed()
# 随机生成3条长度为4的数据
inputs = torch.randn([3,4])
print('Input is:', inputs)
# 实例化模型
model = model_LR(4)
outputs = model(inputs)
print('Output is:', outputs)

从输出结果看,模型最终的输出g(⋅)恒为0.5。这是由于采用全0初始化后,不论输入值的大小为多少,Logistic函数的输入值恒为0,因此输出恒为0.5。

问题1:Logistic回归在不同的书籍中,有许多其他的称呼,具体有哪些?你认为哪个称呼最好?
答:对数几率回归,逻辑斯蒂回归。对数几率回归更好,通过这个名字就可以看出它采用的是对数几率的函数,可以使人们对这个分类方法进行直观的了解,简洁明了。

问题2:什么是激活函数?为什么要用激活函数?常见激活函数有哪些?
答:
激活函数是一种添加到人工神经网络中的函数,旨在帮助网络学习数据中的复杂模式。在神经元中,输入的input经过一系列加权求和后作用于另一个函数,这个函数就是这里的激活函数。
激活函数(Activation functions)对于人工神经网络模型去学习、理解非常复杂和非线性的函数来说具有十分重要的作用。它们将非线性特性引入到我们的网络中。其主要目的是将A-NN模型中一个节点的输入信号转换成一个输出信号。该输出信号现在被用作堆叠中下一个层的输入。如果我们不运用激活函数的话,则输出信号将仅仅是一个简单的线性函数。但是现实生活中的很多问题都是非线性的。现如今,线性方程是很容易解决的,但是它们的复杂性有限,并且从数据中学习复杂函数映射的能力更小。一个没有激活函数的神经网络将只不过是一个线性回归模型(Linear regression Model)罢了,它功率有限,并且大多数情况下执行得并不好。我们希望我们的神经网络不仅仅可以学习和计算线性函数,而且还要比这复杂得多。同样是因为没有激活函数,我们的神经网络将无法学习和模拟其他复杂类型的数据,例如图像、视频、音频、语音等。这就是为什么我们要使用人工神经网络技术,诸如深度学习(Deep learning),来理解一些复杂的事情,一些相互之间具有很多隐藏层的非线性问题,而这也可以帮助我们了解复杂的数据。
常见的激活函数有:(1)Sigmoid函数或者Logistic函数(2)Tanh — Hyperbolic tangent(双曲正切函数)(3)ReLu -Rectified linear units(线性修正单元)

3.1.3 损失函数

在模型训练过程中,需要使用损失函数来量化预测值和真实值之间的差异。
给定一个分类任务,y表示样本x的标签的真实概率分布,向量y^=p(y|x)表示预测的标签概率分布。
训练目标是使得y^尽可能地接近y,通常可以使用交叉熵损失函数。
在给定y的情况下,如果预测的概率分布y^与标签真实的分布y越接近,则交叉熵越小;如果p(x)和y越远,交叉熵就越大。
对于二分类任务,我们只需要计算y=p(y=1|x),用1−y来表示p(y=0|x)。
给定有N个训练样本的训练集{(x(n),y(n))}Nn=1,使用交叉熵损失函数,Logistic回归的风险函数计算方式为:
R(w,b)=−1N∑n=1N(y(n)logy(n)+(1−y(n))log(1−y(n)))。(3.4)
向量形式可以表示为:
R(w,b)=−1N(yTlogy+(1−y)Tlog(1−y)),(3.5)
其中y∈[0,1]N为N个样本的真实标签构成的N维向量,y^为N个样本标签为1的后验概率构成的N维向量。
二分类任务的交叉熵损失函数的代码实现如下:

# 实现交叉熵损失函数
class BinaryCrossEntropyLoss(op):def __init__(self):self.predicts = Noneself.labels = Noneself.num = Nonedef __call__(self, predicts, labels):return self.forward(predicts, labels)def forward(self, predicts, labels):"""输入:- predicts:预测值,shape=[N, 1],N为样本数量- labels:真实标签,shape=[N, 1]输出:- 损失值:shape=[1]"""self.predicts = predictsself.labels = labelsself.num = self.predicts.shape[0]loss = -1. / self.num * (torch.matmul(self.labels.t(), torch.log(self.predicts)) + torch.matmul((1-self.labels.t()), torch.log(1-self.predicts)))loss = torch.squeeze(loss, 1)return loss# 测试一下
# 生成一组长度为3,值为1的标签数据
labels = torch.ones([3,1])
# 计算风险函数
bce_loss = BinaryCrossEntropyLoss()
print(bce_loss(outputs, labels))

3.1.4 模型优化

不同于线性回归中直接使用最小二乘法即可进行模型参数的求解,Logistic回归需要使用优化算法对模型参数进行有限次地迭代来获取更优的模型,从而尽可能地降低风险函数的值。

在机器学习任务中,最简单、常用的优化算法是梯度下降法。使用梯度下降法进行模型优化,首先需要初始化参数W和 b,然后不断地计算它们的梯度,并沿梯度的反方向更新参数。

3.1.4.1 梯度计算

在Logistic回归中,风险函数R(w,b) 关于参数w和b的偏导数为:

通常将偏导数的计算过程定义在Logistic回归算子的backward函数中,代码实现如下:

class model_LR(op):def __init__(self, input_dim):super(model_LR, self).__init__()# 存放线性层参数self.params = {}# 将线性层的权重参数全部初始化为0self.params['w'] = torch.zeros([input_dim, 1])# self.params['w'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, 1])# 将线性层的偏置参数初始化为0self.params['b'] = torch.zeros([1])# 存放参数的梯度self.grads = {}self.X = Noneself.outputs = Nonedef __call__(self, inputs):return self.forward(inputs)def forward(self, inputs):self.X = inputs# 线性计算score = torch.matmul(inputs, self.params['w']) + self.params['b']# Logistic 函数self.outputs = logistic(score)return self.outputsdef backward(self, labels):"""输入:- labels:真实标签,shape=[N, 1]"""N = labels.shape[0]# 计算偏导数self.grads['w'] = -1 / N * torch.matmul(self.X.t(), (labels - self.outputs))self.grads['b'] = -1 / N * torch.sum(labels - self.outputs)

3.1.4.2 参数更新

在计算参数的梯度之后,我们按照下面公式更新参数:

其中α 为学习率。

将上面的参数更新过程包装为优化器,首先定义一个优化器基类Optimizer,方便后续所有的优化器调用。在这个基类中,需要初始化优化器的初始学习率init_lr,以及指定优化器需要优化的参数。代码实现如下:

from abc import abstractmethod# 优化器基类
class Optimizer(object):def __init__(self, init_lr, model):"""优化器类初始化"""# 初始化学习率,用于参数更新的计算self.init_lr = init_lr# 指定优化器需要优化的模型self.model = model@abstractmethoddef step(self):"""定义每次迭代如何更新参数"""pass

然后实现一个梯度下降法的优化器函数SimpleBatchGD来执行参数更新过程。其中step函数从模型的grads属性取出参数的梯度并更新。代码实现如下:

class SimpleBatchGD(Optimizer):def __init__(self, init_lr, model):super(SimpleBatchGD, self).__init__(init_lr=init_lr, model=model)def step(self):# 参数更新# 遍历所有参数,按照公式(3.8)和(3.9)更新参数if isinstance(self.model.params, dict):for key in self.model.params.keys():self.model.params[key] = self.model.params[key] - self.init_lr * self.model.grads[key]

3.1.5 评价指标

在分类任务中,通常使用准确率(Accuracy)作为评价指标。如果模型预测的类别与真实类别一致,则说明模型预测正确。准确率即正确预测的数量与总的预测数量的比值:

其中I(⋅)是指示函数。代码实现如下:

def accuracy(preds, labels):"""输入:- preds:预测值,二分类时,shape=[N, 1],N为样本数量,多分类时,shape=[N, C],C为类别数量- labels:真实标签,shape=[N, 1]输出:- 准确率:shape=[1]"""# 判断是二分类任务还是多分类任务,preds.shape[1]=1时为二分类任务,preds.shape[1]>1时为多分类任务if preds.shape[1] == 1:# 二分类时,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0# 使用'paddle.cast'将preds的数据类型转换为float32类型preds = torch.tensor(preds>=0.5, dtype=torch.float32)else:# 多分类时,使用'paddle.argmax'计算最大元素索引作为类别preds = torch.argmax(preds,1, int32)return torch.mean(torch.tensor(torch.eq(preds, labels), dtype=torch.float32))
# 假设模型的预测值为[[0.],[1.],[1.],[0.]],真实类别为[[1.],[1.],[0.],[0.]],计算准确率
preds = torch.tensor([[0.],[1.],[1.],[0.]])
labels = torch.tensor([[1.],[1.],[0.],[0.]])
print("accuracy is:", accuracy(preds, labels))

3.1.6 完善Runner类

基于RunnerV1,本章的RunnerV2类在训练过程中使用梯度下降法进行网络优化,模型训练过程中计算在训练集和验证集上的损失及评估指标并打印,训练过程中保存最优模型。代码实现如下:

# 用RunnerV2类封装整个训练过程
class RunnerV2(object):def __init__(self, model, optimizer, metric, loss_fn):self.model = modelself.optimizer = optimizerself.loss_fn = loss_fnself.metric = metric# 记录训练过程中的评价指标变化情况self.train_scores = []self.dev_scores = []# 记录训练过程中的损失函数变化情况self.train_loss = []self.dev_loss = []def train(self, train_set, dev_set, **kwargs):# 传入训练轮数,如果没有传入值则默认为0num_epochs = kwargs.get("num_epochs", 0)# 传入log打印频率,如果没有传入值则默认为100log_epochs = kwargs.get("log_epochs", 100)# 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"save_path = kwargs.get("save_path", "best_model.pdparams")# 梯度打印函数,如果没有传入则默认为"None"print_grads = kwargs.get("print_grads", None)# 记录全局最优指标best_score = 0# 进行num_epochs轮训练for epoch in range(num_epochs):X, y = train_set# 获取模型预测logits = self.model(X)# 计算交叉熵损失trn_loss = self.loss_fn(logits, y).item()self.train_loss.append(trn_loss)# 计算评价指标trn_score = self.metric(logits, y).item()self.train_scores.append(trn_score)# 计算参数梯度self.model.backward(y)if print_grads is not None:# 打印每一层的梯度print_grads(self.model)# 更新模型参数self.optimizer.step()dev_score, dev_loss = self.evaluate(dev_set)# 如果当前指标为最优指标,保存该模型if dev_score > best_score:self.save_model(save_path)print(f"best accuracy performence has been updated: {best_score:.5f} --> {dev_score:.5f}")best_score = dev_scoreif epoch % log_epochs == 0:print(f"[Train] epoch: {epoch}, loss: {trn_loss}, score: {trn_score}")print(f"[Dev] epoch: {epoch}, loss: {dev_loss}, score: {dev_score}")def evaluate(self, data_set):X, y = data_set# 计算模型输出logits = self.model(X)# 计算损失函数loss = self.loss_fn(logits, y).item()self.dev_loss.append(loss)# 计算评价指标score = self.metric(logits, y).item()self.dev_scores.append(score)return score, lossdef predict(self, X):return self.model(X)def save_model(self, save_path):torch.save(self.model.params, save_path)def load_model(self, model_path):self.model.params = torch.load(model_path)

3.1.7 模型训练

下面进行Logistic回归模型的训练,使用交叉熵损失函数和梯度下降法进行优化。
使用训练集和验证集进行模型训练,共训练 500个epoch,每隔50个epoch打印出训练集上的指标。
代码实现如下:

# 固定随机种子,保持每次运行结果一致
torch.seed()# 特征维度
input_dim = 2
# 学习率
lr = 0.1# 实例化模型
model = model_LR(input_dim=input_dim)
# 指定优化器
optimizer = SimpleBatchGD(init_lr=lr, model=model)
# 指定损失函数
loss_fn = BinaryCrossEntropyLoss()
# 指定评价方式
metric = accuracy# 实例化RunnerV2类,并传入训练配置
runner = RunnerV2(model, optimizer, metric, loss_fn)runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500, log_epochs=50, save_path="best_model.pdparams")

可视化观察训练集与验证集的准确率和损失的变化情况。

# 可视化观察训练集与验证集的指标变化情况
def plot(runner,fig_name):plt.figure(figsize=(10,5))plt.subplot(1,2,1)epochs = [i for i in range(len(runner.train_scores))]# 绘制训练损失变化曲线plt.plot(epochs, runner.train_loss, color='#e4007f', label="Train loss")# 绘制评价损失变化曲线plt.plot(epochs, runner.dev_loss, color='#f19ec2', linestyle='--', label="Dev loss")# 绘制坐标轴和图例plt.ylabel("loss", fontsize='large')plt.xlabel("epoch", fontsize='large')plt.legend(loc='upper right', fontsize='x-large')plt.subplot(1,2,2)# 绘制训练准确率变化曲线plt.plot(epochs, runner.train_scores, color='#e4007f', label="Train accuracy")# 绘制评价准确率变化曲线plt.plot(epochs, runner.dev_scores, color='#f19ec2', linestyle='--', label="Dev accuracy")# 绘制坐标轴和图例plt.ylabel("score", fontsize='large')plt.xlabel("epoch", fontsize='large')plt.legend(loc='lower right', fontsize='x-large')plt.tight_layout()plt.savefig(fig_name)plt.show()plot(runner,fig_name='linear-acc.pdf')

从输出结果可以看到,在训练集与验证集上,loss得到了收敛,同时准确率指标都达到了较高的水平,训练比较充分。

3.1.8 模型评价

使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率和loss数据。代码实现如下:

score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))

可视化观察拟合的决策边界 Xw+b=0。

def decision_boundary(w, b, x1):w1, w2 = wx2 = (- w1 * x1 - b) / w2return x2
plt.figure(figsize=(5,5))
# 绘制原始数据
plt.scatter(X[:, 0].tolist(), X[:, 1].tolist(), marker='*', c=y.tolist())w = model.params['w']
b = model.params['b']
x1 = torch.linspace(-2, 3, 1000)
x2 = decision_boundary(w, b, x1)
# 绘制决策边界
plt.plot(x1.tolist(), x2.tolist(), color="red")
plt.show()

3.2 基于Softmax回归的多分类任务

Logistic回归可以有效地解决二分类问题。但在分类任务中,还有一类多分类问题,即类别数C大于2 的分类问题。Softmax回归就是Logistic回归在多分类问题上的推广。

3.2.1 数据集构建

我们首先构建一个简单的多分类任务,并构建训练集、验证集和测试集。
本任务的数据来自3个不同的簇,每个簇对一个类别。我们采集1000条样本,每个样本包含2个特征。

数据集的构建函数make_multi的代码实现如下:

import numpy as np
import torchdef make_multiclass_classification(n_samples=100, n_features=2, n_classes=3, shuffle=True, noise=0.1):"""生成带噪音的多类别数据输入:- n_samples:数据量大小,数据类型为int- n_features:特征数量,数据类型为int- shuffle:是否打乱数据,数据类型为bool- noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声输出:- X:特征数据,shape=[n_samples,2]- y:标签数据, shape=[n_samples,1]"""# 计算每个类别的样本数量n_samples_per_class = [int(n_samples / n_classes) for k in range(n_classes)]for i in range(n_samples - sum(n_samples_per_class)):n_samples_per_class[i % n_classes] += 1# 将特征和标签初始化为0X = torch.zeros([n_samples, n_features])y = torch.zeros([n_samples])# 随机生成3个簇中心作为类别中心centroids = torch.randperm(2 ** n_features)[:n_classes]centroids_bin = np.unpackbits(centroids.numpy().astype('uint8')).reshape((-1, 8))[:, -n_features:]centroids = torch.tensor(centroids_bin)# 控制簇中心的分离程度centroids = 1.5 * centroids - 1# 随机生成特征值X[:, :n_features] = torch.randn([n_samples, n_features])stop = 0# 将每个类的特征值控制在簇中心附近for k, centroid in enumerate(centroids):start, stop = stop, stop + n_samples_per_class[k]# 指定标签值y[start:stop] = k % n_classesX_k = X[start:stop, :n_features]# 控制每个类别特征值的分散程度A = 2 * torch.rand([n_features, n_features]) - 1X_k[...] = torch.matmul(X_k, A)X_k += centroidX[start:stop, :n_features] = X_k# 如果noise不为None,则给特征加入噪声if noise > 0.0:# 生成noise掩膜,用来指定给那些样本加入噪声noise_mask = torch.rand([n_samples]) < noisefor i in range(len(noise_mask)):if noise_mask[i]:# 给加噪声的样本随机赋标签值y[i] = torch.randint(n_classes, [1])# 如果shuffle为True,将所有数据打乱if shuffle:idx = torch.randperm(X.shape[0])X = X[idx]y = y[idx]return X, y

随机采集1000个样本,并进行可视化。

import matplotlib.pyplot as plt# 固定随机种子,保持每次运行结果一致
torch.seed()
# 采样1000个样本
n_samples = 1000
X, y = make_multiclass_classification(n_samples, n_features=2, n_classes=3, noise=0.2)# 可视化生产的数据集,不同颜色代表不同类别
plt.figure(figsize=(5,5))
plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())
plt.savefig('linear-dataset-vis2.pdf')
plt.show()

将实验数据拆分成训练集、验证集和测试集。其中训练集640条、验证集160条、测试集200条。

num_train = 640
num_dev = 160
num_test = 200X_train, y_train = X[:num_train], y[:num_train]
X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]# 打印X_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)

这样,我们就完成了Multi1000数据集的构建。

# 打印前5个数据的标签
print(y_train[:5])

3.2.2 模型构建

在Softmax回归中,对类别进行预测的方式是预测输入属于每个类别的条件概率。与Logistic 回归不同的是,Softmax回归的输出值个数等于类别数C,而每个类别的概率值则通过Softmax函数进行求解。

3.2.2.1 Softmax函数

Softmax函数可以将多个标量映射为一个概率分布。对于一个K维向量,x=[x1,⋯,xK],Softmax的计算公式为

在Softmax函数的计算过程中,要注意上溢出和下溢出的问题。假设Softmax 函数中所有的xk都是相同大小的数值a,理论上,所有的输出都应该为1k。但需要考虑如下两种特殊情况:

a为一个非常大的负数,此时exp(a) 会发生下溢出现象。计算机在进行数值计算时,当数值过小,会被四舍五入为0。此时,Softmax函数的分母会变为0,导致计算出现问题;
a为一个非常大的正数,此时会导致exp(a)发生上溢出现象,导致计算出现问题。
为了解决上溢出和下溢出的问题,在计算Softmax函数时,可以使用xk−max(x)代替xk。 此时,通过减去最大值,xk最大为0,避免了上溢出的问题;同时,分母中至少会包含一个值为1的项,从而也避免了下溢出的问题。

Softmax函数的代码实现如下:

def softmax(X):"""输入:- X:shape=[N, C],N为向量数量,C为向量维度"""x_max = torch.max(X, dim=1, keepdim=True)[0]  #N, 1x_exp = torch.exp(X - x_max)partition = torch.sum(x_exp, 1, keepdim=True) #N, 1return x_exp / partition# 观察softmax的计算方式
X = torch.tensor([[0.1, 0.2, 0.3, 0.4],[1,2,3,4]])
predict = softmax(X)
print(predict)

思考题3:Logistic函数是激活函数。Softmax函数是激活函数么?谈谈你的看法。
答:是。 Softmax 是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为 K 的任意实向量,Softmax 可以将其压缩为长度为 K,值在(0,1)范围内,并且向量中元素的总和为 1 的实向量。Softmax 激活函数的不足:在零点不可微;负输入的梯度为零,这意味着对于该区域的激活,权重不会在反向传播期间更新,因此会产生永不激活的死亡神经元。

3.2.2.2 Softmax回归算子

在Softmax回归中,类别标签y∈{1,2,…,C}。给定一个样本x,使用Softmax回归预测的属于类别c的条件概率为

其中wc是第 c 类的权重向量,bc是第 c 类的偏置。
Softmax回归模型其实就是线性函数与Softmax函数的组合。
将N个样本归为一组进行成批地预测。

其中X∈RN×D为N个样本的特征矩阵,W=[w1,……,wC]为C个类的权重向量组成的矩阵,Y^∈RC为所有类别的预测条件概率组成的矩阵。

我们根据公式(3.13)实现Softmax回归算子,代码实现如下:

class model_SR(op):def __init__(self, input_dim, output_dim):super(model_SR, self).__init__()self.params = {}# 将线性层的权重参数全部初始化为0self.params['W'] = torch.zeros([input_dim, output_dim])# self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])# 将线性层的偏置参数初始化为0self.params['b'] = torch.zeros([output_dim])self.outputs = Nonedef __call__(self, inputs):return self.forward(inputs)def forward(self, inputs):"""输入:- inputs: shape=[N,D], N是样本数量,D是特征维度输出:- outputs:预测值,shape=[N,C],C是类别数"""# 线性计算score = torch.matmul(inputs, self.params['W']) + self.params['b']# Softmax 函数self.outputs = softmax(score)return self.outputs# 随机生成1条长度为4的数据
inputs = torch.randn([1,4])
print('Input is:', inputs)
# 实例化模型,这里令输入长度为4,输出类别数为3
model = model_SR(input_dim=4, output_dim=3)
outputs = model(inputs)
print('Output is:', outputs)

从输出结果可以看出,采用全0初始化后,属于每个类别的条件概率均为1C。这是因为,不论输入值的大小为多少,线性函数f(x;W,b)的输出值恒为0。此时,再经过Softmax函数的处理,每个类别的条件概率恒等。
思考题:Logistic函数是激活函数。Softmax函数是激活函数么?谈谈你的看法。
是。
Softmax函数是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为K KK的任意实向量,Softmax函数可以将其压缩为长度为K ,值在[0,1]范围内,并且向量中元素的总和为1的实向量。

3.2.3 损失函数

Softmax回归同样使用交叉熵损失作为损失函数,并使用梯度下降法对参数进行优化。通常使用C维的one-hot类型向量y∈{0,1}C来表示多分类任务中的类别标签。对于类别c,其向量表示为:

其中I(⋅)是指示函数,即括号内的输入为“真”,I(⋅)=1;否则,I(⋅)=0。
给定有N个训练样本的训练集{(x(n),y(n))}Nn=1,令y^(n)=softmax(WTx(n)+b)为样本x(n)在每个类别的后验概率。多分类问题的交叉熵损失函数定义为:

观察上式,y(n)c在c为真实类别时为1,其余都为0。也就是说,交叉熵损失只关心正确类别的预测概率,因此,上式又可以优化为:

其中y(n)是第n个样本的标签。

因此,多类交叉熵损失函数的代码实现如下:

class MultiCrossEntropyLoss(op):def __init__(self):self.predicts = Noneself.labels = Noneself.num = Nonedef __call__(self, predicts, labels):return self.forward(predicts, labels)def forward(self, predicts, labels):"""输入:- predicts:预测值,shape=[N, 1],N为样本数量- labels:真实标签,shape=[N, 1]输出:- 损失值:shape=[1]"""self.predicts = predictsself.labels = labelsself.num = self.predicts.shape[0]loss = 0for i in range(0, self.num):index = self.labels[i]loss -= torch.log(self.predicts[i][index])return loss / self.num# 测试一下
# 假设真实标签为第1类
labels = torch.tensor([0])
# 计算风险函数
mce_loss = MultiCrossEntropyLoss()
print(mce_loss(outputs, labels))

3.2.4 模型优化

使用梯度下降法进行参数学习。

3.2.4.1 梯度计算

计算风险函数R(W,b)关于参数W和b的偏导数。在Softmax回归中,计算方法为:

其中X∈RN×D为N个样本组成的矩阵,y∈RN为N个样本标签组成的向量,y^∈RN为N个样本的预测标签组成的向量,1为N维的全1向量。

将上述计算方法定义在模型的backward函数中,代码实现如下:

class model_SR(op):def __init__(self, input_dim, output_dim):super(model_SR, self).__init__()self.params = {}# 将线性层的权重参数全部初始化为0self.params['W'] = torch.zeros([input_dim, output_dim])# self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])# 将线性层的偏置参数初始化为0self.params['b'] = torch.zeros(output_dim)# 存放参数的梯度self.grads = {}self.X = Noneself.outputs = Noneself.output_dim = output_dimdef __call__(self, inputs):return self.forward(inputs)def forward(self, inputs):self.X = inputs# 线性计算score = torch.matmul(self.X, self.params['W']) + self.params['b']# Softmax 函数self.outputs = softmax(score)return self.outputsdef backward(self, labels):"""输入:- labels:真实标签,shape=[N, 1],其中N为样本数量"""# 计算偏导数N =labels.shape[0]labels = torch.nn.functional.one_hot(labels, self.output_dim)self.grads['W'] = -1 / N * torch.matmul(self.X.t(), (labels-self.outputs))self.grads['b'] = -1 / N * torch.matmul(torch.ones(shape=[N]), (labels-self.outputs))

3.2.4.2 参数更新

在计算参数的梯度之后,我们使用3.1.4.2中实现的梯度下降法进行参数更新。

3.2.5 模型训练

实例化RunnerV2类,并传入训练配置。使用训练集和验证集进行模型训练,共训练500个epoch。每隔50个epoch打印训练集上的指标。代码实现如下:

# 固定随机种子,保持每次运行结果一致
torch.seed()# 特征维度
input_dim = 2
# 类别数
output_dim = 3
# 学习率
lr = 0.1# 实例化模型
model = model_SR(input_dim=input_dim, output_dim=output_dim)
# 指定优化器
optimizer = SimpleBatchGD(init_lr=lr, model=model)
# 指定损失函数
loss_fn = MultiCrossEntropyLoss()
# 指定评价方式
metric = accuracy
# 实例化RunnerV2类
runner = RunnerV2(model, optimizer, metric, loss_fn)# 模型训练
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500, log_eopchs=50, eval_epochs=1,save_path="best_model.pdparams")# 可视化观察训练集与验证集的准确率变化情况
plot(runner, fig_name='linear-acc2.pdf')
plt.show()

best accuracy performence has been updated: 0.78750 --> 0.79375
best accuracy performence has been updated: 0.79375 --> 0.80000
[Train] epoch: 150, loss: 0.42924508452415466, score: 0.7875000238418579
[Dev] epoch: 150, loss: 0.4625350534915924, score: 0.800000011920929
[Train] epoch: 200, loss: 0.42312106490135193, score: 0.7890625
[Dev] epoch: 200, loss: 0.45930060744285583, score: 0.800000011920929
best accuracy performence has been updated: 0.80000 --> 0.80625
[Train] epoch: 250, loss: 0.4200384318828583, score: 0.792187511920929
[Dev] epoch: 250, loss: 0.4580235183238983, score: 0.8062499761581421
[Train] epoch: 300, loss: 0.41835641860961914, score: 0.792187511920929
[Dev] epoch: 300, loss: 0.45751485228538513, score: 0.8062499761581421
[Train] epoch: 350, loss: 0.4173872172832489, score: 0.7906249761581421
[Dev] epoch: 350, loss: 0.4573286175727844, score: 0.800000011920929
[Train] epoch: 400, loss: 0.41680675745010376, score: 0.7906249761581421
[Dev] epoch: 400, loss: 0.457279771566391, score: 0.800000011920929
[Train] epoch: 450, loss: 0.4164491593837738, score: 0.7906249761581421
[Dev] epoch: 450, loss: 0.45728760957717896, score: 0.800000011920929

3.2.6 模型评价

使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率。代码实现如下:

score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))

可视化观察类别划分结果。

# 均匀生成40000个数据点
x1, x2 = torch.meshgrid(torch.linspace(-3.5, 2, 200), torch.linspace(-4.5, 3.5, 200))
x = torch.stack([torch.flatten(x1), torch.flatten(x2)], axis=1)
# 预测对应类别
y = runner.predict(x)
y = torch.argmax(y, axis=1)
# 绘制类别区域
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)torch.seed()
n_samples = 1000
X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)plt.scatter(X[:, 0].tolist(), X[:, 1].tolist(), marker='*', c=y.tolist())
plt.show()

3.3 实践:基于Softmax回归完成鸢尾花分类任务

3.3.1 数据处理

3.3.1.1 数据集介绍

Iris数据集,也称为鸢尾花数据集,包含了3种鸢尾花类别(Setosa、Versicolour、Virginica),每种类别有50个样本,共计150个样本。其中每个样本中包含了4个属性:花萼长度、花萼宽度、花瓣长度以及花瓣宽度,本实验通过鸢尾花这4个属性来判断该样本的类别。

3.3.1.2 数据清洗

缺失值分析
对数据集中的缺失值或异常值等情况进行分析和处理,保证数据可以被模型正常读取。代码实现如下:

from sklearn.datasets import load_iris
import pandas
import numpy as npiris_features = np.array(load_iris().data, dtype=np.float32)
iris_labels = np.array(load_iris().target, dtype=np.int32)
print(pandas.isna(iris_features).sum())
print(pandas.isna(iris_labels).sum())

从输出结果看,鸢尾花数据集中不存在缺失值的情况。

异常值处理
通过箱线图直观的显示数据分布,并观测数据中的异常值。

import matplotlib.pyplot as plt #可视化工具# 箱线图查看异常值分布
def boxplot(features):feature_names = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']# 连续画几个图片plt.figure(figsize=(5, 5), dpi=200)# 子图调整plt.subplots_adjust(wspace=0.6)# 每个特征画一个箱线图for i in range(4):plt.subplot(2, 2, i+1)# 画箱线图plt.boxplot(features[:, i], showmeans=True, whiskerprops={"color":"#E20079", "linewidth":0.4, 'linestyle':"--"},flierprops={"markersize":0.4},meanprops={"markersize":1})# 图名plt.title(feature_names[i], fontdict={"size":5}, pad=2)# y方向刻度plt.yticks(fontsize=4, rotation=90)plt.tick_params(pad=0.5)# x方向刻度plt.xticks([])plt.savefig('ml-vis.pdf')plt.show()boxplot(iris_features)

从输出结果看,数据中基本不存在异常值,所以不需要进行异常值处理。

3.3.1.3 数据读取

本实验中将数据集划分为了三个部分:
训练集:用于确定模型参数;
验证集:与训练集独立的样本集合,用于使用提前停止策略选择最优模型;
测试集:用于估计应用效果(没有在模型中应用过的数据,更贴近模型在真实场景应用的效果)。
在本实验中,将80%的数据用于模型训练,10%的数据用于模型验证,10%的数据用于模型测试。代码实现如下:

# 加载数据集
def load_data(shuffle=True):"""加载鸢尾花数据输入:- shuffle:是否打乱数据,数据类型为bool输出:- X:特征数据,shape=[150,4]- y:标签数据, shape=[150]"""# 加载原始数据X = np.array(load_iris().data, dtype=np.float32)y = np.array(load_iris().target, dtype=np.int32)X = torch.tensor(X)y = torch.tensor(y)# 数据归一化X_min = torch.min(X, 0)[0]X_max = torch.max(X, 0)[0]X = (X-X_min) / (X_max-X_min)# 如果shuffle为True,随机打乱数据if shuffle:idx = torch.randperm(X.shape[0])X = X[idx]y = y[idx]return X, y# 固定随机种子
torch.seed()num_train = 120
num_dev = 15
num_test = 15X, y = load_data(shuffle=True)
print("X shape: ", X.shape, "y shape: ", y.shape)
X_train, y_train = X[:num_train], y[:num_train]
X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]

# 打印X_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)

# 打印前5个数据的标签
print(y_train[:5])

3.3.2 模型构建

使用Softmax回归模型进行鸢尾花分类实验,将模型的输入维度定义为4,输出维度定义为3。代码实现如下:

# 输入维度
input_dim = 4
# 类别数
output_dim = 3
# 实例化模型
model = model_SR(input_dim=input_dim, output_dim=output_dim)

3.3.3 模型训练

实例化RunnerV2类,使用训练集和验证集进行模型训练,共训练80个epoch,其中每隔10个epoch打印训练集上的指标,并且保存准确率最高的模型作为最佳模型。代码实现如下:

# 学习率
lr = 0.2# 梯度下降法
optimizer = SimpleBatchGD(init_lr=lr, model=model)
# 交叉熵损失
loss_fn = MultiCrossEntropyLoss()
# 准确率
metric = accuracy# 实例化RunnerV2
runner = RunnerV2(model, optimizer, metric, loss_fn)# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=200, log_epochs=10, save_path="best_model.pdparams")


可视化观察训练集与验证集的准确率变化情况。

plot(runner,fig_name='linear-acc3.pdf')

3.3.4 模型评价

使用测试数据对在训练过程中保存的最佳模型进行评价,观察模型在测试集上的准确率情况。代码实现如下:

# 加载最优模型
runner.load_model('best_model.pdparams')
# 模型评价
score, loss = runner.evaluate([X_test, y_test])

[Test] score/loss : 0.7850/0.9471

3.3.5 模型预测

使用保存好的模型,对测试集中的数据进行模型预测,并取出1条数据观察模型效果。代码实现如下:

# 预测测试集数据
logits = runner.predict(X_test)
# 观察其中一条样本的预测结果
pred = torch.argmax(logits[0]).numpy()
# 获取该样本概率最大的类别
label = y_test[0].numpy()
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label, pred))

The true category is 1 and the predicted category is 1

3.4 小结

本节实现了Logistic回归和Softmax回归两种基本的线性分类模型。

3.5 实验拓展

为了加深对机器学习模型的理解,请自己动手完成以下实验:
尝试调整学习率和训练轮数等超参数,观察是否能够得到更高的精度;
1、将学习率调整维0.015,训练次数增加至200

[Test] score/loss:0.5255/0.7037

2、将学习率调整维0.2,训练次数增加至250

[Test] score/loss:0.6560/0.8452

3、将学习率调整维0.01,训练次数增加至300

[Test] score/loss:0.7986/1.3267

总结:了解了Logistic回归的多种称呼以及激活函数的定义和它存在的必要性。了解了Softmax函数是激活函数。通过调整学习率和训练轮数等超参数,可以能够得到更高的精度。

NNDL 实验四 线性分类相关推荐

  1. NNDL实验四 线性分类

    前言:在做这次实验的时候是在Kaggle上运行的,因为Kaggle上有小小的免费的GPU加速,同时小小的体会到了在训练模型的时候,GPU加速和CPU加速之间的一种区别.Kaggle是一个很好的网站,有 ...

  2. HBU-NNDL 实验四 线性分类

    第3章 线性分类 线性回归和线性分类之间有着很大的联系,从某种意义上来说,线性分类就是线性回归函数使用激活函数的结果.同时也可以看成是线性回归降维的结果.对于一个线性回归函数,我们可以通过添加全局函数 ...

  3. 计算机计算线性卷积规则,实验四----线性卷积与圆周卷积的计算.doc

    实验三 线性卷积与圆周卷积的计算 实验目的 1.掌握计算机的使用方法和常用系统软件及应用软件的使用. 2.通过编程,上机调试程序,进一步增强使用计算机解决问题的能力. 3.掌握线性卷积与循环卷积软件实 ...

  4. 河北工业大学数据挖掘实验四 贝叶斯决策分类算法

    贝叶斯决策分类算法 一.实验目的 二.实验原理 1.先验概率和类条件概率 2.贝叶斯决策 三.实验内容和步骤 1.实验内容 2.实验步骤 3.程序框图 4.实验样本 5.实验代码 四.实验结果 五.实 ...

  5. 数学-机器学习-线性分类

    四 线性分类 4.1 简介 4.1.1 思维导图 线性分类-思维导图 4.2.2 线性分类 线性回归 f ( w , b ) = w T x + b , x ∈ R p f(w,b)=w^{T}x+b ...

  6. 深度学习 第3章线性分类 实验四 pytorch实现 Logistic回归 上篇

    目录: 第3章 线性分类 3.1 基于Logistic回归的二分类任务 3.1.1 数据集构建 3.1.2 模型构建 1. Logistic函数 2. Logistic回归算子 3.1.3 损失函数 ...

  7. 深度学习 第3章线性分类 实验四 pytorch实现 Softmax回归 鸢尾花分类任务 下篇

    目录: 第3章 线性分类 3.3 实践:基于Softmax回归完成鸢尾花分类任务 3.3.1 数据处理 3.3.1.1 数据集介绍 3.3.1.2 数据清洗 1. 缺失值分析 2. 异常值处理 3.3 ...

  8. 神经网络与深度学习(四)线性分类

    目录 3.1 基于Logistic回归的二分类任务 3.1.1 数据集构建 3.1.2 模型构建 3.1.3 损失函数 3.1.4 模型优化 3.1.4.1 梯度计算 3.1.4.2 参数更新 3.1 ...

  9. NNDL 实验五 前馈神经网络(1)二分类任务

    目录 前言 一.4.1 神经元 4.1.1 净活性值 [思考题]加权相加与仿射变换之间有什么区别和联系? 4.1.2 激活函数 动手实现<神经网络与深度学习>4.1节中提到的其他激活函数: ...

最新文章

  1. PHP最简单写文件记日志当前时间
  2. 楼层平面放线及标高实测记录_没去过工地?没有施工经验?市政工程施工测量放线方法总结!...
  3. [firefox] Scrapbook Plus的改进版Scrapbook X
  4. boost::mpl模块实现set相关的测试程序
  5. centos7 kafka2.3.1单机伪集群部署
  6. elasticsearch启动报错:master not discovered yet
  7. NetSuite 财务科目合并
  8. ##实验 1-3 GVRP 配置
  9. Connext DDSQoS参考
  10. python英文文本分析和提取_英文文本挖掘预处理流程总结
  11. Oracle数据库同义词详解
  12. (4.1.40.5)Android手势检测GestureDecetor详解
  13. Tarena - 基础查询
  14. PMP 11.27 考试倒计时37天!来提分啦!
  15. 人工智能科技如何为航空业“保驾护航”?
  16. C++没落了?学习C++没有前途了?从业者给你揭晓答案
  17. 使用Python和OpenCV标记超级像素的炫彩度
  18. odoo列表字段颜色,行颜色,many2many_tags颜色,看板颜色
  19. ABBYY OCR技术教电脑阅读缅甸语(上)
  20. android短视频技术,Android短视频开发都需要什么技术?

热门文章

  1. 03 内生性和稳健性检验
  2. Unity prefab
  3. 04@Docker Compose安装Compose模板文件的使用
  4. 芬兰:网上性骚扰可能要坐牢
  5. 恒生电子mysql面试题_【杭州恒生电子面试】面试题_面试经验_面试流程-看准网...
  6. docker和虚拟技术比较
  7. 火猴实现计时动画(firemonkey)
  8. 从建造狗窝到建造帝国大厦——软件开发过程其实可以这么看
  9. SIP协议的NAT穿越技术
  10. 清华版三年级上册计算机教案,清华版三年级上册信息技术教案