跳到主要内容
Jason Lee
The owner of this blog site

I'm a programmer who loves to learn new technologies and build cool stuff. I'm currently working as a software engineer at a small company in Shanghai. Hope to find some connections with you!

View All Authors

vue和django项目的开发/生产环境配置管理

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

前言

虽然教程作者提到了一些分开管理两种环境的做法,但是我这边没有形成完整可复制的做法。借助 chatgpt,将收获整理出来。

vuejs

在项目的根目录下,分别创建两个文件:.env.development and .env.production。最基础的设置是 api 地址:

# .env.development
VITE_API_URL=http://dev-api.example.com

# .env.production
VITE_API_URL=http://api.example.com

任何设置都有以 VITE_开头。为了区别使用配置,在vite.config.js中,有如下:

export default defineConfig({
// 开发环境使用 .env.development 文件
envDir: './',
build: {
// 生产环境使用 .env.production 文件
envFile: '.env.production',
},
});

django

相对于 vue, django 的设置就要复杂一些了。这里把 gpt 关于生产和开发环境的不同列出来,虽然有很多在这个项目没有实现,但是可以为以后更复杂的真实项目做参考。

在 Django 项目中,开发环境和生产环境通常需要进行不同的设置。下面是一些常见的设置差异:

调试模式(Debug Mode):

开发环境:设置 DEBUG = True,以便在出现错误时显示详细的错误信息。 生产环境:设置 DEBUG = False,以避免显示敏感信息和详细的错误信息。 数据库设置:

开发环境:通常使用本地数据库(如 SQLite)进行开发和测试。 生产环境:使用更稳定和可靠的数据库服务器(如 PostgreSQL、MySQL、Oracle)。 静态文件设置:

开发环境:通常将静态文件存储在本地,并通过 Django 的 staticfiles 应用程序来提供静态文件服务。 生产环境:可以使用 CDN(内容分发网络)来提供静态文件,以减轻服务器负载并提高速度。 日志设置:

开发环境:可以设置较详细的日志记录级别,以便更容易调试和跟踪问题。 生产环境:通常使用更严格的日志记录级别,并将日志信息定向到适当的日志文件,以便进行监视和故障排除。 密钥和敏感信息:

开发环境:可以在本地设置中直接存储密钥和敏感信息。 生产环境:建议使用环境变量或其他安全的方式来存储密钥和敏感信息,以保护生产环境的安全性。 域名设置:

开发环境:可以使用本地的开发服务器(如 localhost)进行开发和测试。 生产环境:设置正确的域名,并配置 Web 服务器(如 Nginx 或 Apache)以将请求转发到 Django 应用程序。 缓存设置:

开发环境:可以禁用缓存或使用简单的缓存配置,以方便开发和调试。 生产环境:使用更高效的缓存后端(如 Memcached 或 Redis)来提高性能。 这些只是一些常见的设置差异,实际上还可以根据项目需求和部署环境的要求进行更多的设置和调整。

本项目中,在根目录下创建了三个文件,settings_common.py, settings_dev.py, settings_prod.py。其中 common 是两种环境之间共享的通用配置,然后在 dev 和 prod 中分别引用 common:

from .settings_common import *

然后在 dev 和 prod 各自写一些配置(比如数据库等)。之后修改启动文件manage.py:

import os
import sys

if os.environ.get('DJANGO_ENV') == 'production':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings_prod')
else:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings_dev')

def main():
# 启动逻辑...

if __name__ == '__main__':
main()

如果要部署,记得把环境变量添加上,即设置 DJANGO_ENV 环境变量为 'production'. 而且如果是 uwsgi 作为 django 的服务器的话,还有修改wsgi.py

总结

前端 vue 的环境区别很简单,后端 django 的稍复杂些。主要分三步走:编写配置文件->修改启动文件->部署设置环境变量。

django+nginx+mysql 三容器部署

· 阅读需 4 分钟
Jason Lee
The owner of this blog site

前言

之前的部署直接采用默认的 sqlite3 作为数据库。因为 sqlite3 是文件式数据库,所以打造容器时,本地调试过程中的数据记录也被包含进去。另外,生产环境中,不可能使用 sqlite。因此,在之前的 django+nginx 的基础上,引入了 mysql 容器。

镜像设置

首先在根目录创建一个MySQL数据库,下面有 data.sql 和 Dockerfile。这里我们先单独启动一下 mydql 容器。这时候 mysql 容器里已经有 django 的数据库了(database)。这是因为 data.sql 已经提前创建好了数据库并设置为当前数据库:

CREATE DATABASE IF NOT EXISTS my_database;

USE my_database;

而 Dockerfile 的内容为:

FROM mysql:8

ENV MYSQL_ROOT_PASSWORD pass
COPY ./data.sql /docker-entrypoint-initdb.d/data.sql

最后一行使得 data.sql 在容器初始化时就被执行。这时候虽然有了数据库但是没有表格,需要去 django 项目下,执行python manage.py migrate来在库里面创建表格。运行docker exec -it <name> /bin/bash,进入容器 terminal,进入 mysql 命令行,show tables,可以得到:

16875289399621687528939092.png

说明,数据表格创建成功了。

django settings.py 变动

由于之前是默认的 sqlite3,配置非常简单,只需要指定数据库文件位置。在 django 的 settings.py 中,关于 mysql 数据库的设定为:

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'my_database',
'HOST': 'database', #compose中mysql容器的服务名称
'PORT': '3306',
'USER': 'root',
'PASSWORD': 'pass',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
},
}
}

docker-compose

由于我们是全 docker 运行,因此也要实验一下将 django 项目打包成 docker 容器后,能否也能与 mysql 容器通信且成功创建表格。这次用 compose 实现。

version: "3.9"
services:
nginx:
build: ./wey-frontend/
restart: always
ports:
- "8000:80"
volumes:
- web_static:/var/www/mysite/assets/static
- web_media:/var/www/mysite/assets/media
depends_on:
- django

django:
build: ./wey_backend/
restart: always
expose:
- 8000
command: >
sh -c "python manage.py collectstatic --noinput
&& python manage.py migrate
&& uwsgi --ini uwsgi.ini"
volumes:
- web_static:/var/www/mysite/assets/static
- web_media:/var/www/mysite/assets/media
depends_on:
- database

database:
build: ./MySQL/
ports:
- '3306:3306'
# environment:
# MYSQL_DATABASE: 'appdb'
# MYSQL_PASSWORD: 'root'
# MYSQL_ROOT_PASSWORD: 'root'
# volumes:
# - 'db:/var/lib/mysql'
# # command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci']

volumes:
web_static:
web_media:

总结

用 mysql 作为数据库,更符合实际生产场景。由于一开始 mysql 容器内没有数据库,因此需要在 Dockerfile 中创建自定义数据库并且使用。django 的 settings.py 中也要作相应调整。docker-compose 中,需要在创建 django 容器后,执行python manage.py migrate,来迁移数据库(即在自定义数据库中创建表来供上线后数据放置)。

wey: 基于vue+django的社交网站(0)

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

前言

这个项目我是跟随 youtube 上的博主视频教程做的。大概是从五月初开始的,持续了三个星期左右。这个项目对我来说有着非凡的意义,因为能将它完成说明自己具备了设计一个网站和部署上线的能力,而且用上了我目前最喜欢的技术栈:vuejs+django。写这篇博客时,它还处于一个比较难用的阶段,很多功能还没有添加,我打算在记录的过程中逐步添加这些功能。

项目 github 链接

工作流

写前端代码的编辑器为 vscode,插件组合为 Tailwind CSS IntelliSense+Volar。后端代码用 pycharm,python 的环境管理参照我之前写的博文。很不幸的是,自己创建项目时没有引入 git 进行代码管理。然后我现在已经加上了 git。

资源网站:

完成记录

  • day1: 将数据库更换为 mysql,并用 docker 部署运行。暂时删除了注册账户时发送邮件的功能。批量生成 post 和 comment 的伪数据。
  • day2: 实现了帖子和评论的分页。管理后台界面用了 django-admin-grappeli 插件进行美化(其实差不多)。修复了 edit profile 无法显示头像上传框的问题。登录逻辑由原来的双 token 改为单 token。
  • day3: 整理了生产环境和开发环境配置文件,并在生产环境(云主机+docker)调试上线。

我的python开发环境管理实践

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

前言

前段时间做项目,摸索出了一套比较稳定且适合自己的 python 环境管理方案,适用于 win10/11。

2024 年更新:这个节点上,主流有四种环境管理工具:venv(也就是本文推荐的),virtualenv(貌似在 windows 上不友好,比较繁琐),conda(辣鸡,不考虑),poetry(号称现代化的 python 环境管理工具,以后可以尝试)。当时写的 venv 的用法还是有些不对的地方,故修改一下。

venv 的使用

我在之前很长一段时间里用的是 anaconda 作为包管理器。但是一些缺点让我放弃了它:首先 anaconda 太庞大了,其次它的 conda 库经常找不到一些库或者网络速度极慢。后来我转到了 venv,venv 和 conda 一样,虚拟环境被包含在一个文件夹中,里面有一个脚本文件用于激活。

每个项目可以用 venv 创建自己独有的环境。为了统一和简介,依赖的文件夹名都叫做'venv'。打开 cmd,并切换到文件夹下,新建环境的命令如下:

python -m venv venv

这样就创建了一个文件夹,内部目录为:

16873086912601687308690642.png

其中,Lib 装了环境,Scripts 有激活和退出环境的脚本。以 powershell 为例,输入脚本路径,就激活了环境:

.\venv\Scripts\activate.ps1

退出:deactivate. 通过pip list,新环境预装的包为:

16873091022651687309101499.png

可以说什么都没有,具有很大的灵活性了,完全由用户掌控。

vscode 和 pycharm 选择创建好的 venv

如果是一些小脚本,单独运行的那种,我会选择 vscode;如果用到了 django 等创建一个项目,则用 pycharm。

首先说说 vscode。右下角选择 venv 解释器路径:

16873102812591687310281154.png

然后是 pycharm,同样是右下角,add new interpreter->existing->选择解释器位置:

16873105482641687310547564.png

这时候要新建一个 terminal,就会自动将环境挂载上。

逐步让Google收录本博客

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

前言

这个博客建立快满一年了,到目前为止积累了 77 篇文章,都是原创。建立之初以为半年之内 Google 会自动收录,没想到现在依旧未收录。之前有手动向 Google 提交过网址,但是好像是所有权验证问题失败了。这一次,我打算纪录下从开始提交到最终收录的全过程,顺便可以学到一点 seo 的知识。本文将持续更新。

第一天

登录 google search console,并验证了网站所有权(DNS 方式)后,有两个资源:一个是根域名 jasonleehere.com,一个是二级域名 blog.jasonleehere.com。然后向 Google 提交 sitemap(之前已经安装好 hexo-sitemap 插件并生成好 xml 文件)。想要查询 Google 收录的网址和数量,可以在“网页索引编制”中查看:

16871396076941687139607410.png

现在就等明天回来,看看网页是否被收录。

第二天

已收录,可通过在 google 搜索栏键入网址来找到:

16872193526401687219352555.png

16872195266361687219526444.png

前五个结果都是我的网站里的内容。但是,如果直接搜索 jasonleehere 或者 blog jasonleehere 是不会显示的,因为有其他来源的网页占据,一直到了第五页才有我。

这样就是成功了,后续就是看看 google search console 里的数据分析,看看有没有人通过 google 点进我的网站。

第三天

效果栏里开始有数据了。

16873078462601687307845321.png

第四天

非常幸运地,第四天就有了网页索引报告。可以看出,第一批收录了 10 个网页,其余大部分网页处在看见了但暂时未收录的状态。

16873931464181687393145504.png

针对csv文件类型的深度学习全流程记录

· 阅读需 7 分钟
Jason Lee
The owner of this blog site

前言

就像我之前的post里讲的那样,输入形式或者说处理对象的多样化是 ai 从业者面对的挑战之一。这篇文章,我将尽力解决存储在 csv 中的数据,涉及到读取、预处理、写入等环节。

读取

pd.read_csv('path')

预处理

一个 csv 文件,里面的值会是什么样呢?一种是数值形式,一种是文本形式。对于数值形式,有哪些处理方法呢?

缺失值处理:检查数据中是否存在缺失值(NaN 或空值),并根据实际情况进行处理。常见的方法包括删除带有缺失值的样本、用平均值或中位数填充缺失值,或使用插值方法进行填充。

特征缩放:对于数值型特征,进行特征缩放可以帮助模型更好地进行训练。常见的特征缩放方法包括标准化(将数据转换为均值为 0,方差为 1 的分布)、归一化(将数据缩放到 0 和 1 之间的范围)等。

异常值处理:检测和处理异常值可以提高模型的鲁棒性。可以使用统计方法(如均值加减三倍标准差)或基于分布的方法来检测异常值,并根据实际情况进行修正或删除。

离散化/分箱处理:将连续型的数值特征转化为离散型特征,可以在一定程度上提高模型的效果。可以使用等频分箱、等宽分箱等方法将数值范围划分为多个离散的区间,并将数值映射到对应的区间。

特征选择:根据数据的实际情况,选择对目标变量有更强相关性的数值特征进行训练。可以使用相关性分析、特征重要性评估等方法进行特征选择。

数据标准化:对于某些具有明确量纲的特征,例如日期、时间等,可以进行数据标准化,将其转换为合适的数值表示,便于模型理解和处理。

对于非数值型数据,可以采取以下预处理措施:

标签编码:对于具有顺序关系的分类变量,可以使用标签编码将其转换为数值表示。例如,将"低"、"中"、"高"转换为 0、1、2 等。

独热编码:对于没有顺序关系的分类变量,可以使用独热编码将其转换为多个二进制特征表示。独热编码将每个类别转换为一个独立的特征,并且只有一个特征的值为 1,其余特征的值都为 0。这可以避免模型将无序的类别变量视为有序的。

词袋模型/文本向量化:对于文本数据,可以使用词袋模型或文本向量化方法将文本转换为数值表示。词袋模型将文本视为单词的集合,并计算每个单词在文本中的出现频率或使用 TF-IDF 等方法进行加权。文本向量化方法(如 Word2Vec、GloVe 等)可以将单词嵌入到低维向量空间中,保留了单词之间的语义关系。

序列编码:对于具有顺序关系的序列数据,例如时间序列或序列文本,可以使用序列编码方法将其转换为数值表示。常见的序列编码方法包括循环神经网络(RNN)和长短期记忆网络(LSTM)等。

类别合并/分组:对于具有大量类别但类别之间相似度较高的非数值型数据,可以考虑将类别进行合并或分组,减少特征维度的同时保留信息。

异常值处理:对于非数值型数据,也可能存在异常值,例如错误的标签或不一致的类别。可以检测和处理这些异常值,例如进行纠正或删除。

标准化

numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))

数据集的制作

预处理好数据后,将数据转化为 tensor 格式。要将目前的 pd 表格数据转为 tensor,最简单的方法是 torch.tensor():

train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)

这里不是采用继承 torch.utils.Dataset 类的写法,而是直接将 train_features 用一个函数制作数据集:

dataset = torch.utils.data.TensorDataset(*data)

最后包裹一层 DataLoader。

搭建网络

对于网络,可以不用类的写法,而是实现 nn.Sequential 函数:

net = nn.Sequential(
nn.Linear(in_features, 512),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(512, 1),
)

这样,节省了不少代码。

损失和优化器

这里的损失采用了 MSE 函数,由于期望值较大(六位数),因此损失值较大,让我以为训练出错了。另外,如果用 SGD,损失将为 nan。改用 Adam 优化器才能正常训练。推测是因为 SGD 对学习率敏感,学习率不设置的很精确的话,训练直接失败。

完整流程

形成一种规范:代码开头,导入库后,跟着指定默认设备,和超参数。接着开始制作数据集,包括数据的预处理。然后定义网络结构、损失函数、优化器。然后是正式的训练代码。最后测试时,记得将 model 调至测试模式,使得 dropout 能正常工作。

代码地址

总结

之前写 pytorch 的时候,写法很死板。经过这一次练习,接触到了新的写法,比如 dataset 部分和网络部分。

django静态文件管理

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

前言

可以分为开发和部署两个阶段进行介绍。

开发

STATIC_URL = "static/"(默认设置好的),这一项是告诉 django,静态文件存在每一个应用的哪一个文件夹下,方便后面收集静态文件到一处。还有一个用途是,给出资源的引用位置。例如,如果 STATIC_URL 设置为'/static/',则可以通过http://yourdomain.com/static/css/styles.css来访问名为styles.css的CSS文件。

另外,如果某些静态文件不属于任何一个应用,或者所在文件夹名字和 STATIC_URL 不同,可以将文件夹名放在 STATICFILES_DIRS:

STATICFILES_DIRSSTATICFILES_DIRSSTATICFILES_DIRSSTATICFILES_DIRSSTATICFILES_DIRS = [
BASE_DIR / "static",
"/var/www/static/",
]

这两项都和 STATIC_URL 不同。

当需要在开发阶段提供静态文件服务时,可以在根应用的 urls.py 里设置:

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
# ... the rest of your URLconf goes here ...
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

还有一种用户上传的静态文件,用 MEDIA_URL,类似于 STATIC_URL.

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
# ... the rest of your URLconf goes here ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

部署

首先要明确,部署阶段时的静态文件管理到底为什么要和开发阶段区分开来?

在开发阶段,开发人员通常会使用各种工具和技术来编写和测试代码。他们经常需要修改代码和调试,以确保功能的正确实现。在这个阶段,开发人员通常希望在每次修改代码后能够快速地查看结果,以便及时调试和验证功能。为了实现这个目标,他们通常会使用本地开发服务器,在其中运行代码并动态生成页面。在开发过程中,静态文件(如 HTML、CSS、JavaScript、图像等)可能会被直接从开发环境的文件系统加载,或者通过开发服务器进行实时编译和转换。

然而,当代码准备好部署到生产环境时,情况就会有所不同。在部署阶段,我们通常希望优化网站或应用程序的性能和加载速度。这就涉及到对静态文件的管理和优化。

静态文件管理在部署阶段的目标是将这些文件进行优化和准备,以便在生产环境中提供最佳的性能和用户体验。这可能包括压缩和合并 CSS 和 JavaScript 文件、优化图像大小和格式、设置缓存策略以减少加载时间等。

另外,静态文件管理还可以包括将文件上传到内容分发网络(Content Delivery Network,CDN)上,以确保文件在全球范围内的快速传输和分发。

通过将开发阶段和部署阶段的静态文件管理分离开来,可以使开发人员专注于开发和调试代码,而不必担心性能优化和部署的细节。这也有助于确保在生产环境中获得最佳的性能和用户体验。

STATIC_ROOT:collectstatic 将收集静态文件进行部署的目录的绝对路径。例如: "/var/www/example.com/static/",而搜集的源头就是上述的 STATIC_URL 和 STATICFILES_DIRS。

参考资料

https://docs.djangoproject.com/zh-hans/4.2/ref/settings/#std-setting-STATIC_URL

基于docker的django+vue网站部署

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

前言

部署和开发是两个完全不同的工作。得益于 docker 等虚拟化技术,现在的部署已经简单许多了。尽管作者是用原生 linux 环境做示范,但是我选用 docker 作为部署工具。主要以查找博客和问 chatgpt 来学习,中间由于对 nginx 不了解,还看了相关的视频教程。大概花了三四天时间,从本地 win 环境,到最终的云主机。现在,我终于可以说,自己是一个全栈工程师了,真正从应用的开发到落地都有了粗浅的涉及。

总体流程图

这是两容器之间的配合示意图。外部的请求从宿主机的 8000 端口进来,然后被 docker 投射到容器 1,交给 nginx 处理。nginx 根据请求的 url,判断是动态还是静态请求。如果是静态,则去找 vue 项目打包来的文件夹 dist 内的资源并返回;如果是动态资源,通过 http 方式转发给容器 2 的端口 8000。uwsgi 处理完逻辑后,将可能有的响应转回给 nginx,再返回给用户。

项目结构

wey
├── docker-compose.yml
├── wey-frontend
│ ├── dist
│ ├── Dockerfile
│ ├── index.html
│ ├── nginx.conf
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ ├── README.md
│ ├── src
│ ├── tailwind.config.js
│ └── vite.config.js
└── wey_backend
├── account
├── chat
├── db.sqlite3
├── Dockerfile
├── manage.py
├── media
├── notification
├── pip.conf ## 给容器2的pip换源
├── post
├── requirements.txt
├── scripts
├── search
├── uwsgi.ini
└── wey_backend

容器 1 配置

# nginx.conf

server {
listen 80;
server_name 127.0.0.1;

location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}

location /api/ {
proxy_pass http://django:8000;
}

location /static {
alias '/var/www/mysite/assets/static';
}

location /media {
alias '/var/www/mysite/assets/media';
}

location /admin/ {
proxy_pass http://django:8000;
}

}

对应的 dockerfile:

# 使用一个基础的 Node.js 镜像作为构建环境
FROM node:14 as builder

# 设置工作目录
WORKDIR /app

# 复制 package.json 和 package-lock.json 文件到容器中
COPY package*.json ./

# 安装依赖
RUN npm install

# 复制前端应用源代码到容器中
COPY . .

# 执行构建命令
RUN npm run build

# 创建一个新的容器镜像
FROM nginx:latest

# 复制构建产物到 Nginx 的默认静态文件目录
COPY --from=builder /app/dist /usr/share/nginx/html

# 将自定义的 nginx.conf 文件复制到容器中,覆盖默认配置
# remove the default conf
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 暴露容器的 80 端口
EXPOSE 80


容器 2 配置

[uwsgi]

module = wey_backend.wsgi
py-autoreload=1
http-timeout = 86400

http=0.0.0.0:8000

master=true
processes = 4
threads=2

chmod-socket=666

vaccum=true
die-on-term=true

对应的 dockerfile 配置:

FROM python:3.11
RUN mkdir /code
WORKDIR /code
COPY . /code

COPY pip.conf /root/.pip/pip.conf

# uwsgi setup
RUN pip install uwsgi
RUN pip install -r requirements.txt

EXPOSE 8000

CMD ["uwsgi", "--ini", "uwsgi.ini"]

docker-compose

docker compose 是在有多个容器且容器之间存在依赖关系时适用。它取代的是命令行构建镜像和创建容器的方式,使得部署更简洁。比如在 nginx 中,ports:"8000:80",是在建立端口映射。volumes:是在把特定的目录在整个 docker 应用进程内建立一个索引,实现文件共享。

version: "3.9"
services:
nginx:
build: ./wey-frontend/
restart: always
ports:
- "8000:80"
volumes:
- web_static:/var/www/mysite/assets/static
- web_media:/var/www/mysite/assets/media
depends_on:
- django


django:
build: ./wey_backend/
restart: always
expose:
- 8000
command: >
sh -c "python manage.py collectstatic --noinput
&& uwsgi --ini uwsgi.ini"
volumes:
- web_static:/var/www/mysite/assets/static
- web_media:/var/www/mysite/assets/media

volumes:
web_static:
web_media:

总结

这篇文章实现了用 docker 来部署 django+vue 的前后端分离网站,并用 docker-compose 来简化了部署。

为什么中间遭遇了较大的挫折并且一度想放弃呢?反思的结果是,自己一开始就把多个容器放在一起考虑,导致头绪纷乱无章。后面在尝试了测试两个容器是否能独立运行,然后联合运行,才成功。

pytorch tutorial 笔记+注解

· 阅读需 9 分钟
Jason Lee
The owner of this blog site

前言

pytorch,一个熟悉又陌生的朋友。以前用到它时,常常是哪里不会查哪里;等过一阵子又忘记了用法还得重新查。虽然毕业论文课题用到了框架,但是还处在拾人牙慧的阶段。这一次,经过前段时间 python 和 django 的学习,将提炼出来的学习经验用于 pytorch,先将官方文档的 tutorial 看完,然后看视频从零开始做一个项目。

Tensor

Tensor 和 numpy 的 array 很相似,意味着一些 api 的名字和功能相同。介绍 Tensor 的创建时,官网列举的来源包含:python list、numpy array、继承另一个 tensor 的形状和数据类型、随机/全 1/全 0。但是,在实际训练中,你面对的是这样的 Tensor:

batch_size = 64

# Create data loaders.
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

for X, y in test_dataloader:
print(f"Shape of X [N, C, H, W]: {X.shape}")
print(f"Shape of y: {y.shape} {y.dtype}")
break

X,y 都是 tensor。要理解 X 从哪里来的,我们从自定义 dataset 入手:

class CustomImageDataset(Dataset):
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
self.img_labels = pd.read_csv(annotations_file)
self.img_dir = img_dir
self.transform = transform
self.target_transform = target_transform

def __len__(self):
return len(self.img_labels)

def __getitem__(self, idx):
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = read_image(img_path)
label = self.img_labels.iloc[idx, 1]
if self.transform:
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label)
return image, label

read_image 的全称是 torchvision.io.read_image,将 jpeg 或者 png 读取为 uint8 的 tensor。在别的实例中,有可能在transform中添加一个 ToTensor(),ToTensor converts a PIL image or NumPy ndarray into a FloatTensor. and scales the image’s pixel intensity values in the range [0., 1.].

tensor 的属性只有三个:shape、dtype、device。其中 shape 和 size()经常搞混,得到错误提示:xxx 数据类型没有 shape 属性/size()方法,这里对他们做一个区分。

在 Python 和 PyTorch 中,以下数据类型具有 shape 属性: NumPy 数组(numpy.ndarray):NumPy 是一个用于科学计算的 Python 库,其数组具有 shape 属性。可以使用 ndarray.shape 来获取数组的形状。 PyTorch 张量(torch.Tensor):PyTorch 是一个深度学习框架,其中的张量对象具有 shape 属性。可以使用 tensor.shape 来获取张量的形状。 需要注意的是,PyTorch 的张量类型包括 CPU 张量和 CUDA 张量,它们都具有 shape 属性。 在 PyTorch 中,size()方法和 shape 属性实际上是等价的,它们都用于获取张量的形状。因此,在 PyTorch 张量上使用 size()方法和使用 shape 属性将返回相同的结果。 在 PyTorch 中,shape 和 size()都返回一个 torch.Size 对象,它是一个元组子类,可以像元组一样进行索引操作。例如,对于形状为(2, 3)的张量,可以通过 shape[0]或 size()[0]来访问第一个维度的大小。 对于其他 Python 数据类型,如元组、列表或字符串,没有 shape 属性或 size()方法,可以使用 len()函数来获取元素的数量或长度。

数据加载

数据加载需要两个类:Dataset 和 DataLoader:

from torch.utils.data import Dataset, DataLoader

Dataset

根据数据集的特点进行数据加载、预处理和标准化等操作。以前面的自定义 dataset 为例,它首先继承了 Dataset 类,然后分别实现了init, len, getitem方法。其中,getitem方法是最重要的,它可以实现文件从硬盘到内存的读取,然后转化为 tensor,以及可选的 transform 预处理步骤。

Dataloader

DataLoader是一个数据加载器类,用于将 Dataset 中的数据分批加载到模型中进行训练或推理。它提供了多线程数据加载、批处理和数据随机打乱等功能。通过使用 DataLoader,你可以方便地迭代整个数据集,并按照指定的批次大小获取数据。

DataLoader 类接收一个 Dataset 对象作为参数,并可以配置以下参数:

  • batch_size:指定每个批次的样本数量。
  • shuffle:指定是否对数据进行随机打乱。
  • num_workers:指定用于数据加载的线程数。

最重要的是,DataLoader 是一种可迭代序列,可以用 next 进行数据的读取:

train_features, train_labels = next(iter(train_dataloader))

在实际代码中,常使用 enumerate()函数。它返回一个生成器对象,该生成器生成索引-元素对:

for batch, (X, y) in enumerate(dataloader):
X, y = X.to(device), y.to(device)

在上面这个例子中,batch 就是第几个 batch 的意思,(X, y)就是 Dataset 返回的元素和标签。所以上述代码将遍历一次整个数据集,按 batch 返回数据和 batch 的索引。

在这里可以引入 epoch 和 batch 的概念。epoch 表示完整扫描整个数据集的次数,batchsize 表示每次 epoch 中,计算一次损失需要读取的样本数量。而 batch 就是样本总数除以 batchsize 的取整。所以,一个 epoch 中会有若干个 batch。

批次大小的选择涉及到多个因素,包括内存限制、计算资源、模型性能等。较大的批次大小可以提高计算效率,但可能需要更多的内存,并且可能导致模型的收敛速度变慢。较小的批次大小可以减少内存占用,但可能导致计算效率降低。 通常,选择适当的批次大小需要进行实验和调整。一般而言,较大的批次大小在具有较大训练集和较强计算能力的情况下可以获得更好的性能,而较小的批次大小对于内存受限的情况或者需要更好的模型泛化能力的情况可能更合适。

读取数据时的 cpu 和 gpu

Dataset 中,数据通常会被预处理、转换为张量等操作,并在 CPU 上进行。然后,通过 DataLoader 将处理后的数据以指定的批量大小加载到内存中,并返回一个或多个批量的迭代器。在训练过程中,可以将这些批量数据移动到 GPU 上,并将其传递给模型进行训练或推断。完整的示例代码:

import torch
from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
def __init__(self, data):
self.data = data

def __len__(self):
return len(self.data)

def __getitem__(self, index):
# 数据预处理和转换操作在CPU上进行
sample = self.data[index]
processed_sample = preprocess(sample)
tensor_sample = torch.tensor(processed_sample, dtype=torch.float32)
return tensor_sample

# 创建数据集
dataset = MyDataset(data)

# 创建数据加载器
batch_size = 32
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# 检查GPU是否可用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 别忘了将网络模型也放到gpu中(如果可用)
model = Model().to(device)

# 在训练过程中,将批量数据移动到GPU上进行计算
for batch in dataloader:
inputs = batch.to(device)
targets = get_targets(batch) # 假设有一个函数用于获取目标值
targets = targets.to(device)

# 在GPU上进行模型的训练或推断
outputs = model(inputs)
loss = loss_function(outputs, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()

子组件外面一层的div为什么要加v-if

· 阅读需 2 分钟
Jason Lee
The owner of this blog site

项目中有这样一段代码:

<div class="p-4 bg-white border border-gray-200 rounded-lg mb-4" v-if="post.id">
<PostItem v-bind:post="post" />
</div>

PostItem 作为子组件被包裹在一层 div 中,外层 div 起到控制样式的作用。如果去掉 v-if,前端报错:PostItem 内无法获得 post 数据。一开始,我想不明白:为什么 post 在 script 中赋值,而不能直接使用?

chatgpt 给出的回答是:

如果去掉子组件外面一层的 div 中的 v-if 指令,可能会导致父组件无法正确地向子组件传值。这是因为 v-if 指令会影响组件的渲染时机,如果去掉 v-if 指令,组件会在父组件渲染时一并渲染,此时父组件传递给子组件的数据可能还没有准备好,导致子组件无法正确获取数据。

当加上 v-if 指令后,只有在 v-if 条件为真时,子组件才会被渲染,这样可以保证父组件传递给子组件的数据已经准备好了。因此,如果要在父组件向子组件传值时,建议在子组件外面加一层带有 v-if 指令的 div 来控制子组件的渲染时机,以保证子组件能够正确地获取父组件传递的数据。