前言
突发奇想想学机器学习,这里是学习过程的笔记
准备
我做了这些准备工作
- MacBook 一台,搭建好Python环境,安装numpy和matplotlib
- 优达学城注册免费的《深度学习》课程(Google合作)
- 廖雪峰Python入门教程学习
- 花费两天时间大致浏览《机器学习实战》
学习这些知识应该足以进行接下来的优达学城的学习
课程一 从机器学习到深度学习
前言小节1-8,主要介绍了深度学习的发展现状等等知识。
image.png小节9-12介绍了softmax模型。
粗略浏览机器学习实战后,在机器学习实战这本书中,大致介绍了机器学习的几种算法。从表面上来看,机器学习是一些分类和聚类算法。在这些算法中,介绍了一种算法,叫做逻辑回归分类。
在小节9-12中,主要介绍了分类器模型——逻辑回归,分类函数使用的是softmax函数。
- 什么是softmax函数
这张图片可以表明什么是softmax函数了。对原来数列中的每个数z求exp(z),新数的大小所占的比例就是新数的softmax概率。
- 性质
如果输入同比例扩大,则分类器的结果越两极化,越自信,如果输入同比例缩小,分类器结果趋于平均,不自信。
- 算法
import numpy as np
def softmax(x):
"""Compute softmax values for each sets of scores in x."""
expList = [np.exp(i) for i in x]
expSum = sum(expList)
x = [i/expSum for i in expList]
return np.array(x)
image.png
13-14节主要讲One-Hot编码。在softmax函数给出一组概率数列之后,如何确定分类呢?例如概率最高的为1,其他的为0,这样的一个数列,属于One-Hot编码。这种编码是已经确定了分类。
交叉熵15-16节讲了交叉熵。softmax可以计算概率数列,OneHot是已经确定的分类,那如何计算概率数列到某个分类的距离呢?使用交叉熵来度量这个距离。
image.png image.png17-20 节讲解了如何使用这个分类器。其中,18节讲了为什么需要采用一种特殊的初始数据。
sum = 1000000000
for i in range(1000000):
sum += 0.000001
sum -= 1000000000
print(sum)
这段代码运行结果不是1。如果把sum换成一个很小的数字,例如1,而不是1000000000,我们发现结果误差变小了。基于这个原因,我们希望初始数据总是均值为0,并且各个方向的方差为一致的。例如一个灰度图片的像素值0-255,我们需要把它减去128,然后除以128,这样每一个数字都是-1到1之间的数字,这样的初始数据更适合用来训练。
image.png这样,我们就可以进行训练了。回顾一下视频内容,xi是训练数据矩阵,w是随机权重矩阵,为了性能,随机值取自正态分布中轴为0,方差很小的分布函数,然后计算概率数列,和目标的距离。然后求出到所有目标的平均距离。我们的目的是让距离变小,所以我们沿着梯度下降的方向优化权重矩阵,同时优化截距b。不断重复这一个过程,直到局部最优为止。
- 安装docker
https://www.docker-cn.com/community-edition#/download
配置官方中国镜像。
image.png- 安装jupyter notebook
$ pip3 install jupyter
$ jupyter notebook
此时可以使用命令jupyter notebook打开一个jupyter编辑器
- tensorflow环境搭建
$ docker run -it -p 8888:8888 tensorflow/tensorflow
运行上述命令会自动下载tensorflow镜像,前提是仓库镜像设置成中国的镜像,否则下载很慢。运行命令后,会提示你打开网页,打开这个网址以后会显示tensorflow的jupyter编辑环境,前提是jupyter notebook安装正确
- 挂载docker的文件目录
我们需要把官方的作业导进去。关闭容器,重新打开容器,使用-v 主机目录:容器目录
来进行挂载。
docker run -v /Users/hahaha/tensorflow/:/notebooks -it -p 8888:8888 tensorflow/tensorflow
其中/Users/hahaha/tensorflow/是我的mac的一个文件夹,notebooks是tensorflow中的jupyter默认编辑目录。
在主机目录的挂载目录下面粘贴第一个作业文件,1_notmnist.ipynb。这个文件可以在这里找到: 1_notmnist.ipynb
作业一内容作业代码段一
首先运行一下第一段代码的import,应该是没有任何出错的,此时什么也不会发生,如果出现了红色的输出错误,那就说明这些from import没有导入成功。
# These are all the modules we'll be using later. Make sure you can import them
# before proceeding further.
from __future__ import print_function
# print函数
import matplotlib.pyplot as plt
# 绘图工具
import numpy as np
# 矩阵计算
import os
# 文件路径
import sys
# 文件输出
import tarfile
# 解压缩
from IPython.display import display, Image
# 显示图片
from scipy import ndimage
# 图像处理
from sklearn.linear_model import LogisticRegression
# 逻辑回归模块线性模型
from six.moves.urllib.request import urlretrieve
# url处理
from six.moves import cPickle as pickle
# 数据处理
# Config the matplotlib backend as plotting inline in IPython
%matplotlib inline
# matplotlib是最著名的Python图表绘制扩展库,
# 它支持输出多种格式的图形图像,并且可以使用多种GUI界面库交互式地显示图表。
# 使用%matplotlib命令可以将matplotlib的图表直接嵌入到Notebook之中,
# 或者使用指定的界面库显示图表,它有一个参数指定matplotlib图表的显示方式。
# inline表示将图表嵌入到Notebook中。
作业代码段二
接下来是第二段代码,会进行下载用于训练和测试的字母集合,大概是300mb大小。下载成功后,可以看到挂载目录下面的这两个文件。
作业url = 'https://commondatastorage.googleapis.com/books1000/'
last_percent_reported = None
data_root = '.' # Change me to store data elsewhere
def download_progress_hook(count, blockSize, totalSize):
"""A hook to report the progress of a download. This is mostly intended for users with
slow internet connections. Reports every 5% change in download progress.
"""
# 钩子函数用来实时显示下载进度
global last_percent_reported
percent = int(count * blockSize * 100 / totalSize)
if last_percent_reported != percent:
if percent % 5 == 0:
sys.stdout.write("%s%%" % percent)
sys.stdout.flush()
else:
sys.stdout.write(".")
sys.stdout.flush()
last_percent_reported = percent
def maybe_download(filename, expected_bytes, force=False):
"""Download a file if not present, and make sure it's the right size."""
dest_filename = os.path.join(data_root, filename)
# data_root是当前目录,在这个目录上加上文件名,设置为要保存的文件位置
if force or not os.path.exists(dest_filename):
# force是强制下载,忽略已经下载的文件
print('Attempting to download:', filename)
filename, _ = urlretrieve(url + filename, dest_filename, reporthook=download_progress_hook)
# 使用urlretrieve来下载文件,挂上钩子
print('\nDownload Complete!')
statinfo = os.stat(dest_filename)
# 获取下载到的文件的信息
if statinfo.st_size == expected_bytes:
# 正确大小
print('Found and verified', dest_filename)
else:
# 错误大小,提示用户使用浏览器下载
raise Exception(
'Failed to verify ' + dest_filename + '. Can you get to it with a browser?')
return dest_filename
train_filename = maybe_download('notMNIST_large.tar.gz', 247336696)
test_filename = maybe_download('notMNIST_small.tar.gz', 8458043)
作业代码段三
解压缩用例
num_classes = 10
# 数字总共有多少个
np.random.seed(133)
# 初始化随机种子
def maybe_extract(filename, force=False):
# 假设已经解压缩了
root = os.path.splitext(os.path.splitext(filename)[0])[0] # remove .tar.gz
# splitext(filename)[0]用于去除一个后缀,用两次就是去除两次后缀,也就是去除.tar.gz这个后缀
if os.path.isdir(root) and not force:
# You may override by setting force=True.
# 已经解压缩了就不再解压缩了
print('%s already present - Skipping extraction of %s.' % (root, filename))
else:
print('Extracting data for %s. This may take a while. Please wait.' % root)
tar = tarfile.open(filename)
sys.stdout.flush()
tar.extractall(data_root)
tar.close()
# 解压缩到当前目录下面
data_folders = [
os.path.join(root, d) for d in sorted(os.listdir(root))
if os.path.isdir(os.path.join(root, d))]
if len(data_folders) != num_classes:
raise Exception(
'Expected %d folders, one per class. Found %d instead.' % (
num_classes, len(data_folders)))
print(data_folders)
# 检查解压缩文件目录的数量与期待是否一致,并且打印解压缩出来文件的目录
return data_folders
train_folders = maybe_extract(train_filename)
test_folders = maybe_extract(test_filename)
问题一
写出代码显示解压缩的文件内容信息
- 参考答案
import random
import matplotlib.image as mpimg
def plot_samples(data_folders, sample_size, title=None):
fig = plt.figure()
# 建立空图像
if title: fig.suptitle(title, fontsize=16, fontweight='bold')
# 加入标题
for folder in data_folders:
# 遍历每个字母
image_files = os.listdir(folder)
image_sample = random.sample(image_files, sample_size)
# 从该字母中随机选取一定数量的图片
for image in image_sample:
image_file = os.path.join(folder, image)
ax = fig.add_subplot(len(data_folders), sample_size, sample_size * data_folders.index(folder) +
image_sample.index(image) + 1)
# 创建一个子图
image = mpimg.imread(image_file)
# 加载子图图片
ax.imshow(image)
# 显示子图图片
ax.set_axis_off()
# 关闭子图坐标线
fig.set_size_inches(18.5, 10.5)
# 设置图片显示的大小
plt.show()
plot_samples(train_folders, 20, 'Train')
plot_samples(test_folders, 20, 'Test')
运行效果如下
训练.png 测试.png可以看出,部分训练数据是有问题的
作业代码段四
这之后需要进行数据的归一化处理,就是让图像的每一个像素由0255变换到-1.01.0,并且持久化到文件中
image_size = 28 # Pixel width and height.
pixel_depth = 255.0 # Number of levels per pixel.
# 图片长宽和图片像素深度
def load_letter(folder, min_num_images):
"""Load the data for a single letter label."""
# 处理一个属于一个字母文件夹下面的文件
image_files = os.listdir(folder)
# 列出该文件夹目录下面的所有文件
dataset = np.ndarray(shape=(len(image_files), image_size, image_size),
dtype=np.float32)
# 创建一个长度为文件个数,宽度和高度为28的
print(folder)
# 打印目录
num_images = 0
# 初始化num_images
for image in image_files:
# 对每一个文件处理
image_file = os.path.join(folder, image)
# 获取完整文件路径
try:
image_data = (ndimage.imread(image_file).astype(float) -
pixel_depth / 2) / pixel_depth
# 读入图像,并且归一化处理
if image_data.shape != (image_size, image_size):
# 检查图像的宽高
raise Exception('Unexpected image shape: %s' % str(image_data.shape))
dataset[num_images, :, :] = image_data
# 读入到数据集合中
num_images = num_images + 1
# 图片序号加一
except IOError as e:
# 如果无法读取文件的话,则忽略该文件
print('Could not read:', image_file, ':', e, '- it\'s ok, skipping.')
dataset = dataset[0:num_images, :, :]
# 如果读进来的文件数量少于最小需要文件数量
if num_images < min_num_images:
raise Exception('Many fewer images than expected: %d < %d' %
(num_images, min_num_images))
# 显示缺少的文件数量
print('Full dataset tensor:', dataset.shape)
# 显示文件数量,图片长宽
print('Mean:', np.mean(dataset))
# 平均值
print('Standard deviation:', np.std(dataset))
# 标准差
return dataset
def maybe_pickle(data_folders, min_num_images_per_class, force=False):
dataset_names = []
for folder in data_folders:
# 对每一个字母文件夹处理
set_filename = folder + '.pickle'
# 设置输出的文件
dataset_names.append(set_filename)
# 设置处理过的文件夹
if os.path.exists(set_filename) and not force:
# You may override by setting force=True.
# 检查是否存在已处理过的文件
print('%s already present - Skipping pickling.' % set_filename)
else:
print('Pickling %s.' % set_filename)
dataset = load_letter(folder, min_num_images_per_class)
# 归一化处理这个文件夹下面的所有图片
try:
with open(set_filename, 'wb') as f:
pickle.dump(dataset, f, pickle.HIGHEST_PROTOCOL)
# 持久化数据,将数据保存在硬盘上,而不是一直放在内存中
except Exception as e:
print('Unable to save data to', set_filename, ':', e)
return dataset_names
train_datasets = maybe_pickle(train_folders, 45000)
test_datasets = maybe_pickle(test_folders, 1800)
问题2
显示处理过的图片
- 参考答案
def plot_samples_2(data_folders, sample_size, title=None):
fig = plt.figure()
# 建立空图像
if title: fig.suptitle(title, fontsize=16, fontweight='bold')
# 加入标题
for folder in data_folders:
# 遍历每个字母
with open(folder, 'rb') as pk_f:
data = pickle.load(pk_f)
for index, image in enumerate(data):
if index < sample_size :
# 从该字母中随机选取一定数量的图片
ax = fig.add_subplot(len(data_folders), sample_size, sample_size * data_folders.index(folder) +
index + 1)
# 加载子图图片
ax.imshow(image)
# 显示子图图片
ax.set_axis_off()
# 关闭子图坐标线
fig.set_size_inches(18.5, 10.5)
# 设置图片显示的大小
plt.show()
plot_samples_2(train_datasets, 20, 'Train')
plot_samples_2(test_datasets, 20, 'Test')
image.png
image.png
问题3
检查每个字母下面的文件数目是否相似。
- 参考答案
file_path = 'notMNIST_large/{0}.pickle'
for ele in 'ABCDEFJHIJ':
with open(file_path.format(ele), 'rb') as pk_f:
# 遍历每一个目录
dat = pickle.load(pk_f)
# 加载这个目录下面的持久化文件
print('number of pictures in {}.pickle = '.format(ele), dat.shape[0])
# 打印相关信息
结果表明数目基本一致。
问题3效果
代码段——数据分割
数据不可能一次性就全部加载到内存中,这里对这些数据进行分割,接下来的这份代码对数据进行了分割
def make_arrays(nb_rows, img_size):
if nb_rows:
dataset = np.ndarray((nb_rows, img_size, img_size), dtype=np.float32)
# 创建一个空集合,数据类型是长rows宽img_size高img_size的矩阵,数据类型是浮点32位
labels = np.ndarray(nb_rows, dtype=np.int32)
# 创建一个标签,数据类型是32位整型,长度是rows
else:
dataset, labels = None, None
return dataset, labels
# 返回创建的数据类型
def merge_datasets(pickle_files, train_size, valid_size=0):
num_classes = len(pickle_files)
# 需要处理的类别数量
valid_dataset, valid_labels = make_arrays(valid_size, image_size)
# 建立有效数据集合,长度为有效长度
train_dataset, train_labels = make_arrays(train_size, image_size)
# 建立训练数据集合,长度为训练长度
vsize_per_class = valid_size // num_classes
tsize_per_class = train_size // num_classes
# 计算给定训练长度和有效长度下每个类别的平均长度
start_v, start_t = 0, 0
# 初始化下标,start_v是有效数据的开始,start_t是训练数据的开始
end_v, end_t = vsize_per_class, tsize_per_class
# 初始化下标,end_v是有效数据的结束,end_t是训练数据的结束
end_l = vsize_per_class + tsize_per_class
# 初始化下标,end_l是字母集合的结束,等于每个类别有效数据的长度+训练数据的长度
for label, pickle_file in enumerate(pickle_files):
# 遍历每一个pickle_file
try:
with open(pickle_file, 'rb') as f:
# 打开这个持久化文件
letter_set = pickle.load(f)
# 加载数据集
# let's shuffle the letters to have random validation and training set
np.random.shuffle(letter_set)
# 打乱数据集的顺序
if valid_dataset is not None:
# 如果不是测试集的话,更新测试集,否则 valid_dataset 不更新
valid_letter = letter_set[:vsize_per_class, :, :]
# numpy切片 http://brieflyx.me/2015/python-module/numpy-array-split/
# 从打乱的数据中选择 每类有效数据 数量的数据进行处理,放到 valid_letter 中
valid_dataset[start_v:end_v, :, :] = valid_letter
# 把这份数据放到valid_dataset中
valid_labels[start_v:end_v] = label
# 标记label 应该是 0~9中的一种
start_v += vsize_per_class
end_v += vsize_per_class
# 更新下标
# 循环结束时, valid_dataset 应该总长度为 valid_size 的一份数据, valid_labels是对应位置的标签
train_letter = letter_set[vsize_per_class:end_l, :, :]
# 除去valid部分的随机其他元素,长度为 end_l - vsize_per_class = tsize_per_class
train_dataset[start_t:end_t, :, :] = train_letter
# 循环结束时,train_dataset应该是总长为 train_size 的 一份数据
#
train_labels[start_t:end_t] = label
start_t += tsize_per_class
end_t += tsize_per_class
# 更新下标
except Exception as e:
print('Unable to process data from', pickle_file, ':', e)
raise
return valid_dataset, valid_labels, train_dataset, train_labels
train_size = 200000
valid_size = 10000
test_size = 10000
valid_dataset, valid_labels, train_dataset, train_labels = merge_datasets(
train_datasets, train_size, valid_size)
_, _, test_dataset, test_labels = merge_datasets(test_datasets, test_size)
print('Training:', train_dataset.shape, train_labels.shape)
print('Validation:', valid_dataset.shape, valid_labels.shape)
print('Testing:', test_dataset.shape, test_labels.shape)
代码段——打散数据
permutation函数介绍:http://www.jianshu.com/p/f0eb10acaa2d
def randomize(dataset, labels):
# labels.shape[0] 是 labels 的长度
permutation = np.random.permutation(labels.shape[0])
# 随机取出这么多数字的打乱
print(labels.shape[0])
shuffled_dataset = dataset[permutation,:,:]
# 打乱数据
shuffled_labels = labels[permutation]
# 打乱标签
return shuffled_dataset, shuffled_labels
train_dataset, train_labels = randomize(train_dataset, train_labels)
test_dataset, test_labels = randomize(test_dataset, test_labels)
valid_dataset, valid_labels = randomize(valid_dataset, valid_labels)
问题4
检验打散后的数据是否正确
- 参考答案
import random
def plot_sample_3(dataset, labels, title):
fig = plt.figure()
plt.suptitle(title, fontsize=16, fontweight='bold')
# 设置标题样式
items = random.sample(range(len(labels)), 200)
# 打散 labels 长的顺序序列
for i, item in enumerate(items):
# 随机取一个
plt.subplot(10, 20, i + 1)
# 画子图
plt.axis('off')
# 关闭坐标轴
plt.title(chr(ord('A') + labels[item]))
# 加标题
plt.imshow(dataset[item])
# 显示对应位置的子图
fig.set_size_inches(18.5, 10.5)
plt.show()
# 显示图片
plot_sample_3(train_dataset, train_labels, 'train dataset suffled')
plot_sample_3(valid_dataset, valid_labels, 'valid dataset suffled')
plot_sample_3(test_dataset, test_labels, 'test dataset suffled')
问题4
省略类似的两图
代码段——保存数据
pickle_file = os.path.join(data_root, 'notMNIST.pickle')
# 输出文件路径
try:
f = open(pickle_file, 'wb')
# 打开这个文件
save = {
'train_dataset': train_dataset,
'train_labels': train_labels,
'valid_dataset': valid_dataset,
'valid_labels': valid_labels,
'test_dataset': test_dataset,
'test_labels': test_labels,
}
# 写入一个字典 string-ndarray
pickle.dump(save, f, pickle.HIGHEST_PROTOCOL)
f.close()
except Exception as e:
print('Unable to save data to', pickle_file, ':', e)
raise
代码段——显示保存数据的大小
statinfo = os.stat(pickle_file)
print('Compressed pickle size:', statinfo.st_size)
问题5
题目的Google翻译
通过构建,此数据集可能包含大量重叠样本,包括验证和测试集中也包含的训练数据! 训练和测试之间的重叠可能会使结果偏斜,如果您希望在没有重叠的环境中使用您的模型,但如果您希望在使用训练样本时再次看到训练样本,那么实际上是可以的。 测量培训,验证和测试样本之间的重叠程度。
可选问题:
数据集之间的重复数据怎么样? (几乎相同的图像)
创建一个消毒验证和测试集,并比较您在随后的作业中的准确性。
大概意思是训练数据不能和测试用的数据重合,否则导致准确度不准
参考代码:
- 仅仅查看重复的图片数量
import hashlib
pickle_file = os.path.join('.', 'notMNIST.pickle')
try:
with open(pickle_file, 'rb') as f:
data = pickle.load(f)
except Exception as e:
print('Unable to open data from', pickle_file, ':', e)
raise
# 自从保存数据后,如果kernel挂了,就可以从本地直接读取,不用重新运行之前的代码
# 如果报错的话,可以在网上搜索报错的异常
def calcOverlap(sourceSet, targetSet, description):
sourceSetMd5 = np.array([hashlib.md5(img).hexdigest() for img in sourceSet])
# 建立一个md5表格
targetSetMd5 = np.array([hashlib.md5(img).hexdigest() for img in targetSet])
# 建立一个md5表格
overlap = np.intersect1d(sourceSetMd5, targetSetMd5, assume_unique=False)
# 去重
print(description)
print("overlap",overlap.shape[0], "from",sourceSetMd5.shape[0],"to", targetSetMd5.shape[0])
print("rate",overlap.shape[0]*100.0/sourceSetMd5.shape[0],"% and", overlap.shape[0]*100.0/targetSetMd5.shape[0],"%")
# 打印重叠数量
calcOverlap(data['train_dataset'], data['valid_dataset'], "train_dataset & valid_dataset")
calcOverlap(data['train_dataset'], data['test_dataset'], "train_dataset & test_dataset")
calcOverlap(data['test_dataset'], data['valid_dataset'], "test_dataset & valid_dataset")```
![运行效果](https://img.haomeiwen.com/i4388248/2882159fe68dc672.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 去除重复图片资源
待更新
## 问题6
使用逻辑回归训练模型并且进行测试
- 参考代码
import random
def disp_sample_dataset(dataset, labels,trueLabels, title=None):
展示训练的结果
fig = plt.figure()
if title: fig.suptitle(title, fontsize=16, fontweight='bold')
设置标题样式
items = random.sample(range(len(labels)), 200)
随机选择一系列图片
for i, item in enumerate(items):
plt.subplot(10, 20, i + 1)
设置一个子图
plt.axis('off')
关闭坐标线
lab = str(chr(ord('A') + labels[item]))
trueLab = str(chr(ord('A') + trueLabels[item]))
if lab == trueLab:
plt.title( lab )
else:
plt.title(lab + " but " + trueLab)
加上标题
plt.imshow(dataset[item])
显示这个图片
fig.set_size_inches(18.5, 10.5)
plt.show()
def train_and_predict(train_dataset, train_labels, test_dataset, test_labels ,sample_size):
regr = LogisticRegression()
生成训练器
X_train = train_dataset[:sample_size].reshape(sample_size, 784)
根据sample_size选择要训练的数据量
把二维向量压缩到一维向量
y_train = train_labels[:sample_size]
取出训练数据
regr.fit(X_train, y_train)
训练数据
X_test = test_dataset.reshape(test_dataset.shape[0], 28 * 28)
将测试数据压缩到一维向量
y_test = test_labels
测试数据所对应的真实标签
pred_labels = regr.predict(X_test)
生成预测数据
print('Accuracy:', regr.score(X_test, y_test), 'when sample_size=', sample_size)
disp_sample_dataset(test_dataset, pred_labels, test_labels, 'sample_size=' + str(sample_size))
train_and_predict(data['train_dataset'],data['train_labels'],data['test_dataset'],data['test_labels'], 1000)
![image.png](https://img.haomeiwen.com/i4388248/6b3fb8a1d1b1ce34.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
## 模型性能
小节22~27提到了模型性能的相关知识。我们通常希望模型的性能能够达到100%,显然是不可能的。并且,为了使训练集的准确性提高,模型可能会发生过拟合。这时要遵循两点。
- 不要将训练数据一次性使用,而是分块使用,每次训练一部分
- 当模型参数使30个以上的用例由错误变成正确,则这个参数的改变是有效果的。
![模型性能](https://img.haomeiwen.com/i4388248/033910ba1d5c09e3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
## 随机梯度下降
小节29~31讲解了什么是随机梯度下降
在训练过程中,为了让模型朝着最优的方向走,需要计算该点的导数。1.导数的计算量比较大,我们需要随机选择一部分样本来计算导数,来代替真实的导数。这就是随机梯度下降。2.为了减缓随机选择的随机性,我们使用动量的惯性来减少随机性。3.为了让后期模型能够稳定,我们减少学习的步长。
课程一结束
> 作业代码参考
> http://www.hankcs.com/ml/notmnist.html
网友评论