跳到主要内容

类视图的使用

· 阅读需 4 分钟
Jason Lee

前言

对于类视图和函数视图,在我初学django的时,有过这样的迷思:自己定义的类视图似乎和函数视图没有太大的区别,无非就是判断请求方式的时候,CBV用函数,FBV用if...else...。而如果上了框架提供的通用类视图,代码风格似乎过于抽象,且不灵活,无法完全替代函数视图。今天,就以刚刚用函数视图完成的项目为例,尝试将所有函数视图转为类视图,看看过程中有什么新的感悟。

通用类视图里的方法

添加额外的上下文:

def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
# Add in a QuerySet of all the books
context["book_list"] = Book.objects.all()
return context

根据url内传来的query参数来动态查询

这项工作的关键部分是当基于类的视图被调用的时候,各种常用的东西被存储在 self 上,而且请求 (self.request) 根据 URLconf 抓取位置(self.args) 和基于名称 (self.kwargs) 的参数。

def get_queryset(self):
self.publisher = get_object_or_404(Publisher, name=self.kwargs["publisher"])
return Book.objects.filter(publisher=self.publisher)

执行额外的任务

class AuthorDetailView(DetailView):
queryset = Author.objects.all()

def get_object(self):
obj = super().get_object()
# Record the last accessed date
obj.last_accessed = timezone.now()
obj.save()
return obj

通用类视图到底为开发者默认做了哪些事

django框架内置的通用类视图之所以能少些很多代码,很明显是它有一些默认行为。那么这里就记录一下各种常见内置视图的默认行为。

FormView

class ContactFormView(FormView):
template_name = "contact.html"
form_class = ContactForm
success_url = "/thanks/"

def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
form.send_email()
return super().form_valid(form)

表单视图的一般行为是判断请求方式,如果是post,就检验字段有效性,且把错误返回给浏览器,成功则重定向;如果是get就返回待填写的表单。

所以,FormView就是通过暴露一些简单的配置给开发者,比如template_name,其他行为会自动完成。如果有特殊需求,重写get或post方法。form_valid是执行表单验证之后的程序。

form_valid函数是一定要重写的,这也说明django文档里一般都是最小实现形式,不能减少。

总结

  • 类视图完全可以替代函数视图。
  • 优先用内置的通用类视图,首先确认任务类型,如果是展示类,就考虑ListView、DetailVIew;如果是修改类,就是CreateView、UpdateView、DeleteView。
  • 修改类的通用视图,注意表单相关项。运用上面提到的三个函数,可以实现较复杂的需求。
  • 如果不方便,也可以不用内置视图,而采用根据请求方式划分方法,比如get请求。
  • 类视图在采用了内置类的写法后,代码会变少;但更重要的意义是,增加了代码重用的可能性。比如,另外一个views.py可以继承过来。

ModelForm详解

· 阅读需 3 分钟
Jason Lee

前言

web开发中,我目前遇到最复杂、又是最需要的需求,就是用户验证系统。除了一般的注册登录,还有权限、分组等。而从注册开始,需要理解django为什么设计了modelForm来处理用户的表单输入。

从form讲起

在注册的例子中,文档给出的方法是调用如下函数:

from django.contrib.auth.models import User
User.objects.create_user(username='', email='', password='')

但是正常的网站注册方式都是通过表单,怎么建立表单和create_user函数的联系呢?如果搜索文档里的表单,得到的答案是:

from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import NameForm


def get_name(request):
# if this is a POST request we need to process the form data
if request.method == "POST":
# create a form instance and populate it with data from the request:
form = NameForm(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
return HttpResponseRedirect("/thanks/")

# if a GET (or any other method) we'll create a blank form
else:
form = NameForm()

return render(request, "name.html", {"form": form})

我们知道了,对于提交上来的表单,先验证数据是否都有效,然后进一步的处理并重定向。可是什么是进一步的处理?

所以这大概就是文档有时候不能解决实际场景的问题,要结合别人写的代码,再回到文档中理解。

ModelForm

表单,一般都是和模型(也就是表中的字段)绑定的。因此,如果从头由forms.ModelForm继承来的表单类开始写,无疑是一种重复代码。因为与之绑定的模型字段在之前的models.py中定义过了。因此,django创建了ModelForm,直接在表单创建时和模型绑定,这样,提交上来的表单在验证通过后,保存表单就会同时保存提交上来的数据。

关于用户注册,需要知道的是,django自带的User对象已经可以满足大部分场景的使用了,一般直接将其作为模型使用,而不用在models.py中创建。

通过这个例子,我的体会是:django的写法太灵活了,往往一个问题有多种解决方案。可能最好的办法是摸索出一套自己习惯的写法。

tailwindcss配置

· 阅读需 3 分钟
Jason Lee

前言

一个准备实现的 django 项目中用到了 tailwindcss。年初我曾接触过 tcss,但是由于文档对我来说有点看不懂,配置了很久还是失败了,所以搁置了下来。今天,经过良久的尝试,将成功的经验记录下来。

cdn

cdn 是最快尝试 tcss 的方法。它是通过在线引入 tcss 的 stylesheet 来实现的。

<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">

通过 CDN 引入的显著缺点是,每一次更改样式都要请求网络,如果网络不好或者断网,就会带来不好的开发体验。官网文档还给出了一些缺点:

Tailwind CLI

利用脚手架工具,可以生成一个定制的 css 文件,里面预定义了一大堆样式,比如 mt-5 等,正是我们在 html 文件中用到的 tcss 类名。不仅可以离线开发,而且可以添加自定义样式。操作方式如下:

首先,创建一个/src/tcss.css

@tailwind base;
@tailwind components;
@tailwind utilities;

<!-- your style -->
@layer components {
.btn-blue {
@apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}
}

可以看出,tcss 有三个层次,base 层起到清除浏览器的初始默认样式,比如一些 margin 和 padding。components 负责将一些重复使用的样式组提取出来,单独取一个名字,比如上面代码的 btn-blue。最后是 utilities,就是最熟悉的 tcss 自带类,也可以自定义。

创建好上面的源 tcss 文件后,进行解析:

npx tailwindcss-cli build src/tailwind.css -o tailwind.css

上述命令竟然是我从别的教学视频里得到的,官方文档给出的命令只能得到 500 多行的解析文件,只包含了 base 层。不得不说,tcss 文档写的很烂。正常来说,上面命令运行后生成的文件含有 18 多万行代码。

在 index.html 中,header 部分引入生成的tailwind.css:

<link href="/tailwind.css" rel="stylesheet">

可以开始愉快地使用 tcss 了。

django关系字段

· 阅读需 4 分钟
Jason Lee

前言

mysql 断断续续地学,一直对诸如外键的概念不甚清晰,今天做一个备忘。在这篇博文的基础上,加上了一些自己的理解。

建表

#_*_coding:utf-8_*_
from django.db import models

# Create your models here.

class Colors(models.Model):
colors=models.CharField(max_length=10) #蓝色
def __str__(self):
return self.colors

class Ball(models.Model):
color=models.OneToOneField("Colors") #与颜色表为一对一,颜色表为母表
description=models.CharField(max_length=10) #描述
def __str__(self):
return self.description

class Clothes(models.Model):
color=models.ForeignKey("Colors") #与颜色表为外键,颜色表为母表
description=models.CharField(max_length=10) #描述
def __str__(self):
return self.description

class Child(models.Model):
name=models.CharField(max_length=10) #姓名
favor=models.ManyToManyField('Colors') #与颜色表为多对多

一对一(models.OneToOneField)

子表从母表中选出一条数据一一对应,母表中选出来一条就少一条,子表不可以再选择母表中已被选择的那条数据。一般用于某张表的补充,比如用户基本信息是一张表,但并非每一个用户都需要有登录的权限,不需要记录用户名和密码,此时,合理的做法就是新建一张记录登录信息的表,与用户信息进行一对一的关联,可以方便的从子表查询母表信息或反向查询。

## 增加

color_obj=models.Colors.objects.create(colors="黑") #先在母表中创建颜色,并实例化给颜色表对象
models.Ball.objects.create(color=color_obj,description="黑球") #更新Ball表,color字段为颜色表对象,添加description字段

## 删除
models.Ball.objects.get(description="灰球").delete()

## 修改
color_obj=models.Colors.objects.get(colors="黑") #.get()等同于.filter().first()
color_obj.colors="灰"
color_obj.save()
models.Ball.objects.filter(description="黑球").update(color=color_obj,description="灰球")

## 查询
### 子表查询母表
models.Ball.objects.get(description="红球").color.colors

### 母表查询子表
models.Colors.objects.get(colors="红").ball.description

一对多(models.ForeignKey)

子表从母表中选出一条数据一一对应,但母表的这条数据还可以被其他子表数据选择。这里的一和多指的是自身的表的数据在对方表里出现的次数,一次为一,多次为多。比如每个员工归属于一个部门,那么就可以让员工表的部门字段与部门表进行一对多关联,可以查询到一个员工归属于哪个部门,也可反向查出某一部门有哪些员工。

## 查

color_obj=models.Colors.objects.get(colors="红")
color_obj.clothes_set.all()

models.Clothes.objects.filter(color__colors="红")

多对多(models.ManyToManyField)

比如有多个孩子,和多种颜色。每个孩子可以喜欢多种颜色,一种颜色可以被多个孩子喜欢,对于双向均是可以有多个选择。

## 查

#写法1:
child_obj=models.Child.objects.get(name="小明") #写法:子表对象.子表多对多字段.过滤条件(all()/filter())
print(child_obj.favor.all())
#写法2,反向从母表入手:
print(models.Colors.objects.filter(child__name="小明")) #母表对象.filter(子表表名小写__子表字段名="过滤条件")

matplotlib最佳实践

· 阅读需 5 分钟
Jason Lee

前言

跟着油管上的教程学的,作者介绍了一些最佳实践,融合一些自己的思考,把觉得有用的东西记录一下。

part1 折线图

# 导包区
from matplotlib import pyplot as plt
# plt.style.use('seaborn') # 切换图片风格
plt.xkcd() # 手绘风格

# 数据区
ages_x = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
dev_y = [38496, 42000, 46752, 49320, 53200,
56000, 62316, 64928, 67317, 68748, 73752]
py_dev_y = [45372, 48876, 53850, 57287, 63016,
65998, 70003, 70000, 71496, 75370, 83640]

# 绘图区
plt.plot(ages_x, dev_y, color='k', linestyle='--', label='All Devs')
plt.plot(ages_x, py_dev_y, color='b',marker='o', label='Python Devs')

plt.xlabel('Ages')
plt.ylabel('Median Salary')
plt.title('Median Salary By Age')
plt.legend()

plt.grid(True)

plt.show()

plot.plot()函数,一个对应一条曲线,而且,color, linestyle, marker 单独作为参数写,而不是混在一起;还有,把图例放在 plot 函数里而不是 legend(),都是为了增加可读性。

part2 柱状图

画柱状图的函数是 plt.bar()。但是如果将上述的 plt.plot()改成 plt.bar(),会出现重叠的情况。为了避免重叠,需要添加偏移。

ages_x = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
dev_y = [38496, 42000, 46752, 49320, 53200,
56000, 62316, 64928, 67317, 68748, 73752]
py_dev_y = [45372, 48876, 53850, 57287, 63016,
65998, 70003, 70000, 71496, 75370, 83640]
js_dev_y = [37810, 43515, 46823, 49293, 53437,
56373, 62375, 66674, 68745, 68746, 74583]

x_indexes = np.arange(len(ages_x))
width=0.25

width 是柱的宽度,柱的数量*width 应该小等于 1。

plt.bar(x_indexes-width, dev_y, color='k', width=width, label='All Devs')
plt.bar(x_indexes, py_dev_y, color='b', width=width, label='Python Devs')
plt.bar(x_indexes+width, js_dev_y, color='g',width=width, label='JavaScript Devs')

紧接着,作者给出了一个实例,从 csv 文件读取数据,并统计各个编程语言的使用人数。用的是 python 标准 csv 库进行读取,后续可以用 pandas。

with open('data.csv') as csv_file:
csv_reader = csv.DictReader(csv_file)
counter = Counter()

for row in csv_reader:
counter.update(row['LanguagesWorkedWith'].split(';'))

print(counter)
with open('data.csv') as csv_file:
csv_reader = csv.DictReader(csv_file)
counter = Counter()

for row in csv_reader:
counter.update(row['LanguagesWorkedWith'].split(';'))

print(counter)

Counter({'JavaScript': 59219, 'HTML/CSS': 55466, 'SQL': 47544, 'Python': 36443, 'Java': 35917, 'Bash/Shell/PowerShell': 31991, 'C#': 27097, 'PHP': 23030, 'C++': 20524, 'TypeScript': 18523, 'C': 18017, 'Other(s):': 7920, 'Ruby': 7331, 'Go': 7201, 'Assembly': 5833, 'Swift': 5744, 'Kotlin': 5620, 'R': 5048, 'VBA': 4781, 'Objective-C': 4191, 'Scala': 3309, 'Rust': 2794, 'Dart': 1683, 'Elixir': 1260, 'Clojure': 1254, 'WebAssembly': 1015, 'F#': 973, 'Erlang': 777})

画图用 bar 函数的话,x 轴就会很拥挤。所以采用横柱状图 plt.barh(),将 x,y 颠倒。

part3 饼状图

饼状图一般适合五种以下的数据展示。

slices = [59219, 55466, 47544, 36443, 35917]
labels = ['JavaScript', 'HTML/CSS', 'SQL', 'Python', 'Java']
explode = [0, 0, 0, 0.1, 0]

plt.pie(slices, labels=labels, explode=explode,startangle=90, autopct='%1.1f%%', wedgeprops={'edgecolor': 'black'})

## wedgeprops: 楔子属性?反正就是设置图表的一些性质

plt.title('My Awesome Pie Chart')
plt.show()

part4 堆积面积图

堆积面积是 stackplot 的直译,但似乎不是很妥当?api 是 plt.stackplot()

part5 折现填充图

在折现的上方或下方进行填充,直观地表示差距。

plt.plot(ages, py_salaries, label='Python')

part6 直方图

bins 是每根柱代表的数据间距, log 表示将数据取对数

plt.hist(ages, bins=bins, edgecolor='black', log=True)

part7 散点图

plt.scatter(views, likes, c=ratio, cmap='summer', edgecolor='black', linewidth=1, alpha=0.75)
cbar = plt.colorbar()
cbar.set_label('Like/Dislike Ratio')

plt.xscale('log')
plt.yscale('log')

plt.title('Median Salary By Age')
plt.xlabel('View Count')
plt.ylabel('Total Likes')
plt.show()

c 是指颜色的深浅度,cmap 设置颜色的系列,plt.colorbar()在图中添加彩色条,plt.xscale('log')将刻度改为对数,防止过大的数远离图表中心。

part8 时序图

data['Date'] = pd.to_datetime(data['Date']) ##把字符串解析为日期格式
data.sort_values('Date', inplace=True)

price_date = data['Date']
price_close = data['Close']

plt.plot_date(price_date, price_close, linestyle='solid')

plt.gcf().autofmt_xdate() ## 自动格式化日期

part9 动画

这是和深度学习相关的一个场景,比如损失函数的显示就是动态的。


import random
from itertools import count
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

plt.style.use('fivethirtyeight')

x_vals = []
y_vals = []

# plt.plot(x_vals, y_vals)


index = count()

def animate(i):
x_vals.append(next(index))
y_vals.append(random.randint(0, 5))

plt.cla()
plt.plot(x_vals, y_vals)

ani = FuncAnimation(plt.gcf(), animate, interval=1000)
plt.tight_layout()
plt.show()

但是上述代码在 jupyter 中无法正常运行,只能用 pycharm 来输出。

part10 子图

plt.style.use('seaborn')

data = pd.read_csv('data.csv')
ages = data['Age']
dev_salaries = data['All_Devs']
py_salaries = data['Python']
js_salaries = data['JavaScript']

fig1, ax1 = plt.subplots()
fig2, ax2 = plt.subplots()

ax1.plot(ages, dev_salaries, color='#444444',
linestyle='--', label='All Devs')

ax2.plot(ages, py_salaries, label='Python')
ax2.plot(ages, js_salaries, label='JavaScript')

ax1.legend()
ax1.set_title('Median Salary (USD) by Age')
ax1.set_ylabel('Median Salary (USD)')

ax2.legend()
ax2.set_xlabel('Ages')
ax2.set_ylabel('Median Salary (USD)')

plt.tight_layout()

plt.show()

fig1.savefig('fig1.png')
fig2.savefig('fig2.png')

首先,子图中最重要的概念是 figure 和 axis。之前的代码都是针对 figure(默认),figure 可以认为是一个容器,表现为窗口;axis 就是一张具体的图。

pdf转word

· 阅读需 5 分钟

问题描述

学校的系统要求同时上交 pdf 和 word 版本的论文。latex 写的只能生成 pdf,就有了转 word 需求。通过搜索,先后尝试了 pandoc 的 tex2docx(可以生成,但是格式太简陋),adobe 的在线 pdf 转 word(总体美观,但是公式识别失败)。最后,还是选择了 adobe acrobat DC 本地将 pdf 转成 word,并结合 axmath 的 word 插件,输入公式。

acrobat DC 的使用

当初本科时候就装了这个软件。用它输出 word 有两种模式:

保持流动顺序和保持页面布局。其中,保持流动顺序是让段落顺序和 pdf 一样,转换结果的总页数和 pdf 可能不一致;而保持页面布局是强行让文字位置和 pdf 一样,但是丧失了可编辑性。因为后面还要加公式,所以选择第一种,同时也是默认模式。

初步转换后,大概比 pdf 多了七八页。有很多是空白导致的。现在的 word 很粗糙,里面很多隐藏的分割符,加上 word 各种隐藏 bug,所以要耐心。

页码和目录

学校要求是绪论之前(不包括封面)用罗马数字,绪论之后的正文用阿拉伯数字。把绪论之前编为一节,且每一页指定好页码样式,链接到前一节,这样才能自动编号。在绪论第一页开始新的一节,这样就从 1 开始编号了。页码和目录关系很大。这时候更新一下目录,也包括了绪论之前的内容。只能手动删除掉了。

样式管理

来到文字编辑部分。acrobat 会把文档的标题字体给改了,而且正文段落的缩进还各不相同。如果每一段都设置太麻烦了。所以,从“样式”选项卡修改,可以一劳永逸。首先,选择某个样式的文字,点开样式的修改选项卡,如图:

然后,设置好字体和段落,比如宋体、两端对齐,去除缩进等。勾上自动更新,然后相同样式的所有文字都会应用。

注意这个自动更新,如果后面你想对一些格式微调,那么自动更新会让你的微调作用于全局,所以,建议初步改完后,关掉自动更新。完成这一步后,文档大致美观了。

公式输入

现在市面上所有的 pdf 转 word 工具,识别公式的能力都很垃圾。大致排序就是 adobe 在线>acrobat>其他。adobe 也只能识别一些简单的公式,遇到稍微复杂的也是乱码。mathpix 是图片转 latex 的最强工具,识别率非常高,但是收费。我研一时候,买了 axmath,类似于 mathtype,但是功能更丰富,可以将 latex 转为 word 公式。把 overleaf 的公式复制过来,开一个行内公式,粘贴,整理好格式。删空格会碰到很多奇怪的东西,只要多保存,前后试一试,一般都能解决。

更新

axmath 在将 latex 公式转为 word 格式时,会导致文档其他部分出现空白。非常恶心!

Batch Normalization

· 阅读需 7 分钟

前言

Batch Normalization (BN)层,通过将数据批量归一化(使其分布在 N(0,1)),有下列好处:

缓解了梯度传递问题,使模型适应更大的学习率,加速了训练; 改善了饱和非线性模型不易训练的问题; 还起到了正则化的作用。

可以看出,BN 和之前学到的 Xavier 初始化权重的目的类似,都是使训练更容易。

实现

BN 的公式表达为:

$$\mathrm{BN}(\mathbf{x}) = \boldsymbol{\gamma} \odot \frac{\mathbf{x} - \hat{\boldsymbol{\mu}}_\mathcal{B}}{\hat{\boldsymbol{\sigma}}_\mathcal{B}} + \boldsymbol{\beta}.$$

$gamma$ $beta$都是可学习的参数,它们使得 BN 层像一个只有一个神经元的线性层。

import torch
from torch import nn
from d2l import torch as d2l


def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
# 训练模式下,用当前的均值和方差做标准化
X_hat = (X - mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y, moving_mean.data, moving_var.data

class BatchNorm(nn.Module):
# num_features:完全连接层的输出数量或卷积层的输出通道数。
# num_dims:2表示完全连接层,4表示卷积层
def __init__(self, num_features, num_dims):
super().__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(shape)

def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var
# 复制到X所在显存上
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
# 保存更新过的moving_mean和moving_var
Y, self.moving_mean, self.moving_var = batch_norm(
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y

预测和训练时 BN 层的作用方式不同。主要是因为训练时 BN 层是根据批量数据的均值方差来估计整个数据集的均值方差。训练是用了一个滑动估计下来的保留均值方差,BN 层用它来在测试时归一化。

我的实践

在我的弱光照增强网络中,Unet 结构在卷积层中也大量采用了 BN 层。由于显存的限制(8G),我的 batch_size 是 8,EnlightenGAN 作者设为 32。我的担心是,batch_size 设的太小会不会导致 BN 层失效?chatgpt 的回答是:

即使你的 batch_size 很小,使用批量归一化(Batch Normalization)也是有益的,因为它可以使神经网络训练更加稳定和快速。以下是几个原因: 减少内部协变量偏移(Internal Covariate Shift):内部协变量偏移是指在每一层输入的分布随着网络的训练而发生变化,这会导致网络训练变得困难。批量归一化通过在每个小批量上标准化输入,可以有效地减少内部协变量偏移的影响。 提高梯度传播:批量归一化可以缩小每层的激活值范围,使得梯度更容易传播,从而加快训练速度。 增强正则化:批量归一化可以在每个小批量上对激活进行标准化,这相当于对每个激活值都进行了一定的正则化,从而可以减少过拟合的风险。 虽然 batch_size 较小时批量归一化的效果可能不如 batch_size 较大时显著,但仍然可以提高神经网络的性能。因此,我建议在训练神经网络时,无论 batch_size 的大小,都使用批量归一化。

如果将 batch_size 设置为 1,那么使用批量归一化(Batch Normalization)的效果会受到影响,因为它需要在每个小批量上计算均值和方差来进行标准化。当 batch_size 为 1 时,每个小批量只有一个样本,这样计算出来的均值和方差可能不太准确,因此批量归一化的效果可能会降低。 不过,对于某些特殊的任务或架构,有些人可能会在 batch_size 为 1 时使用批量归一化。这时可以考虑使用“自适应批量归一化”(Adaptive Batch Normalization)或“实例归一化”(Instance Normalization)等变种方法,它们可以在 batch_size 为 1 时进行标准化。但需要注意的是,这些方法可能不如标准批量归一化在大批量数据上的效果好,所以在选择是否使用它们时需要根据具体情况进行权衡。

所以,应该还是可以用的。

pytorch装环境

· 阅读需 3 分钟

问题描述

跟李沐学 AI,装环境时,发现无论是本地,还是切到 colab,都出现环境问题。甚至还有奇葩的要求装 gpu,但是 conda 返回的是 cpu 版本的 pytorch。conda 的包容易过期,pip 的包由容易污染环境,甚至一声不吭地卸掉别的包。国内源还出现找不到特定版本 cuda 的 torch。

只有 whl 安装方式是可靠且高效的。首先,whl 纯手动选择版本,其次,挂了 vpn 的情况下,下载速度很快。以后装深度学习环境,我只去 whl 下载。

cuda 门道

对于有 GPU 的机器,需要下载 cuda 取代。然后输入nvidia-smi查看 cuda 版本。如下:

CUDA 分为两种,驱动 API 和运行 API,驱动 API 指的是指的显卡驱动支持的最高 cuda 版本,我们运行程序时用的是运行 API。图片右上角的是去掉 API,显示 11.2。而运行nvcc -V查看运行 API:

10.2. 所以驱动 API 一般高于运行 API。但是,当我们用 conda 虚拟环境时,还有另外一种情况:

这是平时我们所说的 CUDA 版本,由于运行 API 在 CUDA 里的 CUDA Toolkit 工具包中,所以运行 API 版本也是 CUDA Toolkit 工具包的版本。其实装了 Anaconda 之后 Anaconda 也会提供一个 cudatoolkit 工具包,同样包含了 CUDA 的运行 API,可以用来替代官方 CUDA 的 CUDA Toolkit。这也就是为什么有时候我们通过 nvcc-V 查看的 cuda 版本很低(比如 7.5),但是能成功运行 cuda9.0 的 pytorch 的原因。因为在安装完 anaconda 后,运行 pytorch 代码就会使用 anaconda 的 cudatoolkit,而忽视官方的 CUDA Toolkit,所以我们只需要根据 anaconda 的 cudaoolkit 包的版本来安装相应的 pytorch 即可。

conda list查看 cudatoolkit 的版本。这里是 10.2。最终确定要安装的 torch+torchaudio+torchvision 组合一定要是 cuda10.

whl 安装

whl 的网站:https://download.pytorch.org/whl/cu102 。打开搜索,以 torch 为例,输入 torch,找对 python 和操作系统版本,开梯子,下载。然后,在本地命令行内,激活虚拟环境,然后切到下载目录,运行pip install 包名

卷积

· 阅读需 2 分钟

前言

我的研究生方向是图像处理,也用到了深度学习工具,所以这部分相对熟悉一些。在学习过程中,也发现了一些以前没有注意到的知识,比如卷积这个词是怎么从数学转到神经网络的一种操作的。

感受野

卷积核是在局部窗口内操作的,所以,卷积核的大小反映了学习到的范围。浅层的时候,卷积核的感受野只有图像的一小部分;随着网络的变深,后面的卷积核能看到前面的,其感受野最终能大等于整张图。

填充和步幅

目前图像处理最常用的卷积核大小、填充和步幅组合是(3,1,1)。这样的卷积不会使输出特征图的尺寸发生变化。如果要使图像的尺寸缩小,还可以用池化层。

填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的 1/𝑛(𝑛 是一个大于 1 的整数)。

pytorch的基本使用

· 阅读需 4 分钟

之前我们有大量的从零实现一层网络,现在借用先进的pytorch框架,让很多功能得到封装,可以快捷的组建一个块。

nn.Module

nn.Module是pytorch一切网络的祖宗。例如,可以用nn.Sequential()搭建,也可以新写一个类:

class net(nn.Module):
def __init__(self):
super.__init__()
...

def forward(self, x):
...

nn.Sequential和net类都是继承于nn.Module。

参数管理

参数的访问用state_dict()函数:

print(net[2].state_dict())
OrderedDict([('weight', tensor([[ 0.3016, -0.1901, -0.1991, -0.1220,  0.1121, -0.1424, -0.3060,  0.3400]])), ('bias', tensor([-0.0291]))])

每一种参数(比如weight)都是一个类,下面包含数值,梯度等属性。比如:

net[2].weight.data, net[2].weight.grad

权重初始化

默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch的 nn.init模块提供了多种预置初始化方法。用nn.init:

def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]

m就是层的意思,apply函数会将init_normal函数遍历处理所有层。上述代码将所有的全连接层权重初始化为N~(0.0.01)的高斯分布。

共享权重

共享权重似乎在论文中见过很多次,就是将一个子网络训练的过程中,将权重分享给另一个相同结构的子网络。这里给出的,是两个全连接层的参数共享:

# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

创建全新的层

有时候,可能需要创建一个pytorch未实现的层。以自定义一个linear层为例:

class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)

核心步骤就是在init函数,设置好可以更新的权重,然后再forward函数里面定义计算方式。

权重的保存(checkpoint)

pytorch只能保存权重,而不能连同网络结构一起保存,不过听说tf可以。保存方式是torch.save():

假设要保存参数的对应模型是一个MLP:

class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20, 256)
self.output = nn.Linear(256, 10)

def forward(self, x):
return self.output(F.relu(self.hidden(x)))

net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
torch.save(net.state_dict(), 'mlp.params') # 保存
net.load_state_dict(torch.load('mlp.params'))  # 加载