Spaces:
Sleeping
Sleeping
popboat1 commited on
Commit ·
ab81f90
0
Parent(s):
initial commit
Browse files- .gitattributes +4 -0
- Dockerfile +30 -0
- README.md +11 -0
- frontend/3d-visualizer/.gitignore +30 -0
- frontend/3d-visualizer/README.md +36 -0
- frontend/3d-visualizer/eslint.config.mjs +16 -0
- frontend/3d-visualizer/jsconfig.json +7 -0
- frontend/3d-visualizer/next.config.mjs +6 -0
- frontend/3d-visualizer/package-lock.json +0 -0
- frontend/3d-visualizer/package.json +25 -0
- frontend/3d-visualizer/postcss.config.mjs +7 -0
- frontend/3d-visualizer/public/file.svg +1 -0
- frontend/3d-visualizer/public/globe.svg +1 -0
- frontend/3d-visualizer/public/next.svg +1 -0
- frontend/3d-visualizer/public/placeholder.jpg +3 -0
- frontend/3d-visualizer/public/vercel.svg +1 -0
- frontend/3d-visualizer/public/window.svg +1 -0
- frontend/3d-visualizer/src/app/favicon.ico +3 -0
- frontend/3d-visualizer/src/app/globals.css +8 -0
- frontend/3d-visualizer/src/app/layout.js +28 -0
- frontend/3d-visualizer/src/app/page.js +382 -0
- frontend/3d-visualizer/src/components/InputCube.js +39 -0
- frontend/3d-visualizer/src/components/LayerCube.js +93 -0
- frontend/3d-visualizer/src/components/OutputNode.js +18 -0
- requirements.txt +7 -0
- src/.gitignore +17 -0
- src/api/api.py +152 -0
- src/core/layers.py +170 -0
- src/core/losses.py +34 -0
- src/core/optimizers.py +23 -0
- src/model/alexnet.py +201 -0
- src/trainScratchAlexNet.ipynb +270 -0
- src/trainTFalexNet.ipynb +0 -0
- src/utils/im2col.py +54 -0
- src/utils/load_data.py +35 -0
.gitattributes
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.ico filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-alpine AS builder
|
| 2 |
+
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
|
| 5 |
+
COPY frontend/3d-visualizer/package*.json ./
|
| 6 |
+
RUN npm install
|
| 7 |
+
|
| 8 |
+
COPY frontend/3d-visualizer/ ./
|
| 9 |
+
|
| 10 |
+
RUN npm run build
|
| 11 |
+
|
| 12 |
+
FROM python:3.10-slim
|
| 13 |
+
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
RUN apt-get update && apt-get install -y \
|
| 17 |
+
libgl1-mesa-glx \
|
| 18 |
+
libglib2.0-0 \
|
| 19 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 20 |
+
|
| 21 |
+
COPY requirements.txt .
|
| 22 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
COPY src/ ./src/
|
| 25 |
+
|
| 26 |
+
COPY --from=builder /app/frontend/out ./static
|
| 27 |
+
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
CMD ["uvicorn", "src.api:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AlexNet Visualizer
|
| 3 |
+
emoji: 📈
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
short_description: a 3d visualization of alexnet
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
frontend/3d-visualizer/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dependencies
|
| 2 |
+
/node_modules
|
| 3 |
+
/.pnp
|
| 4 |
+
.pnp.js
|
| 5 |
+
|
| 6 |
+
# testing
|
| 7 |
+
/coverage
|
| 8 |
+
|
| 9 |
+
# next.js
|
| 10 |
+
/.next/
|
| 11 |
+
/out/
|
| 12 |
+
|
| 13 |
+
# production
|
| 14 |
+
/build
|
| 15 |
+
|
| 16 |
+
# misc
|
| 17 |
+
.DS_Store
|
| 18 |
+
*.pem
|
| 19 |
+
|
| 20 |
+
# debug
|
| 21 |
+
npm-debug.log*
|
| 22 |
+
yarn-debug.log*
|
| 23 |
+
yarn-error.log*
|
| 24 |
+
|
| 25 |
+
# local env files
|
| 26 |
+
.env*.local
|
| 27 |
+
.env
|
| 28 |
+
|
| 29 |
+
# vercel
|
| 30 |
+
.vercel
|
frontend/3d-visualizer/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
frontend/3d-visualizer/eslint.config.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
|
| 4 |
+
const eslintConfig = defineConfig([
|
| 5 |
+
...nextVitals,
|
| 6 |
+
// Override default ignores of eslint-config-next.
|
| 7 |
+
globalIgnores([
|
| 8 |
+
// Default ignores of eslint-config-next:
|
| 9 |
+
".next/**",
|
| 10 |
+
"out/**",
|
| 11 |
+
"build/**",
|
| 12 |
+
"next-env.d.ts",
|
| 13 |
+
]),
|
| 14 |
+
]);
|
| 15 |
+
|
| 16 |
+
export default eslintConfig;
|
frontend/3d-visualizer/jsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"paths": {
|
| 4 |
+
"@/*": ["./src/*"]
|
| 5 |
+
}
|
| 6 |
+
}
|
| 7 |
+
}
|
frontend/3d-visualizer/next.config.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
output: 'export',
|
| 4 |
+
};
|
| 5 |
+
|
| 6 |
+
export default nextConfig;
|
frontend/3d-visualizer/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/3d-visualizer/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "3d-visualizer",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "eslint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@react-three/drei": "^10.7.7",
|
| 13 |
+
"@react-three/fiber": "^9.5.0",
|
| 14 |
+
"next": "16.2.0",
|
| 15 |
+
"react": "19.2.4",
|
| 16 |
+
"react-dom": "19.2.4",
|
| 17 |
+
"three": "^0.183.2"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@tailwindcss/postcss": "^4",
|
| 21 |
+
"eslint": "^9",
|
| 22 |
+
"eslint-config-next": "16.2.0",
|
| 23 |
+
"tailwindcss": "^4"
|
| 24 |
+
}
|
| 25 |
+
}
|
frontend/3d-visualizer/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
frontend/3d-visualizer/public/file.svg
ADDED
|
|
frontend/3d-visualizer/public/globe.svg
ADDED
|
|
frontend/3d-visualizer/public/next.svg
ADDED
|
|
frontend/3d-visualizer/public/placeholder.jpg
ADDED
|
Git LFS Details
|
frontend/3d-visualizer/public/vercel.svg
ADDED
|
|
frontend/3d-visualizer/public/window.svg
ADDED
|
|
frontend/3d-visualizer/src/app/favicon.ico
ADDED
|
|
Git LFS Details
|
frontend/3d-visualizer/src/app/globals.css
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
body {
|
| 4 |
+
background-color: #000;
|
| 5 |
+
color: white;
|
| 6 |
+
margin: 0;
|
| 7 |
+
overflow: hidden;
|
| 8 |
+
}
|
frontend/3d-visualizer/src/app/layout.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 2 |
+
import "./globals.css";
|
| 3 |
+
|
| 4 |
+
const geistSans = Geist({
|
| 5 |
+
variable: "--font-geist-sans",
|
| 6 |
+
subsets: ["latin"],
|
| 7 |
+
});
|
| 8 |
+
|
| 9 |
+
const geistMono = Geist_Mono({
|
| 10 |
+
variable: "--font-geist-mono",
|
| 11 |
+
subsets: ["latin"],
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
export const metadata = {
|
| 15 |
+
title: "Alex Net Visualizer",
|
| 16 |
+
description: "",
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
export default function RootLayout({ children }) {
|
| 20 |
+
return (
|
| 21 |
+
<html
|
| 22 |
+
lang="en"
|
| 23 |
+
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
| 24 |
+
>
|
| 25 |
+
<body className="min-h-full flex flex-col">{children}</body>
|
| 26 |
+
</html>
|
| 27 |
+
);
|
| 28 |
+
}
|
frontend/3d-visualizer/src/app/page.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
| 4 |
+
import { Canvas } from '@react-three/fiber';
|
| 5 |
+
import { OrbitControls } from '@react-three/drei';
|
| 6 |
+
import { InputCube } from '@/components/InputCube';
|
| 7 |
+
import { LayerCube } from '@/components/LayerCube';
|
| 8 |
+
import { OutputNode } from '@/components/OutputNode';
|
| 9 |
+
|
| 10 |
+
export default function NetworkVisualizer() {
|
| 11 |
+
const [activeTab, setActiveTab] = useState({ type: 'prediction', layerIndex: null });
|
| 12 |
+
const [layerData, setLayerData] = useState([]);
|
| 13 |
+
const [prediction, setPrediction] = useState(null);
|
| 14 |
+
const [previewImage, setPreviewImage] = useState(null);
|
| 15 |
+
const [isVideo, setIsVideo] = useState(false);
|
| 16 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 17 |
+
const [blockGap, setBlockGap] = useState(0.2);
|
| 18 |
+
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
| 19 |
+
const [zoomedFeature, setZoomedFeature] = useState(null);
|
| 20 |
+
|
| 21 |
+
const videoRef = useRef(null);
|
| 22 |
+
const canvasRef = useRef(null);
|
| 23 |
+
const wsRef = useRef(null);
|
| 24 |
+
const isAwaitingResponse = useRef(false);
|
| 25 |
+
const animationFrameId = useRef(null);
|
| 26 |
+
const lastFrameTime = useRef(0);
|
| 27 |
+
const isVideoStateRef = useRef(false);
|
| 28 |
+
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws';
|
| 31 |
+
const wsUrl = `${protocol}//${window.location.host}/ws/predict-video`;
|
| 32 |
+
|
| 33 |
+
wsRef.current = new WebSocket(wsUrl);
|
| 34 |
+
wsRef.current.onmessage = (event) => {
|
| 35 |
+
if (!isVideoStateRef.current) return;
|
| 36 |
+
const data = JSON.parse(event.data);
|
| 37 |
+
setLayerData(data.layers);
|
| 38 |
+
setPrediction(data.prediction);
|
| 39 |
+
isAwaitingResponse.current = false;
|
| 40 |
+
};
|
| 41 |
+
return () => {
|
| 42 |
+
if (wsRef.current) wsRef.current.close();
|
| 43 |
+
cancelAnimationFrame(animationFrameId.current);
|
| 44 |
+
};
|
| 45 |
+
}, []);
|
| 46 |
+
|
| 47 |
+
const processVideoFrame = (timestamp) => {
|
| 48 |
+
if (!videoRef.current || videoRef.current.paused || videoRef.current.ended) return;
|
| 49 |
+
|
| 50 |
+
if (timestamp - lastFrameTime.current >= 80) {
|
| 51 |
+
if (!isAwaitingResponse.current && wsRef.current?.readyState === WebSocket.OPEN) {
|
| 52 |
+
const canvas = canvasRef.current;
|
| 53 |
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
| 54 |
+
ctx.drawImage(videoRef.current, 0, 0, 227, 227);
|
| 55 |
+
const base64Frame = canvas.toDataURL('image/jpeg', 0.4);
|
| 56 |
+
isAwaitingResponse.current = true;
|
| 57 |
+
wsRef.current.send(base64Frame);
|
| 58 |
+
lastFrameTime.current = timestamp;
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
animationFrameId.current = requestAnimationFrame(processVideoFrame);
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
const layout = useMemo(() => {
|
| 65 |
+
const defaultData = [
|
| 66 |
+
{ layer_index: 1, shape: [55, 55, 96], texture_b64: null },
|
| 67 |
+
{ layer_index: 2, shape: [27, 27, 256], texture_b64: null },
|
| 68 |
+
{ layer_index: 3, shape: [13, 13, 384], texture_b64: null },
|
| 69 |
+
{ layer_index: 4, shape: [13, 13, 384], texture_b64: null },
|
| 70 |
+
{ layer_index: 5, shape: [13, 13, 256], texture_b64: null }
|
| 71 |
+
];
|
| 72 |
+
|
| 73 |
+
const activeData = layerData.length > 0 ? layerData : defaultData;
|
| 74 |
+
let currentX = -6;
|
| 75 |
+
|
| 76 |
+
const inputPos = previewImage ? [currentX, 0, 0] : null;
|
| 77 |
+
if (previewImage) currentX += 0.1 + (blockGap * 0.5);
|
| 78 |
+
|
| 79 |
+
const mappedLayers = activeData.map((layer) => {
|
| 80 |
+
const sizeY = Math.max(0.8, layer.shape[0] * 0.07);
|
| 81 |
+
const sizeZ = Math.max(0.8, layer.shape[1] * 0.07);
|
| 82 |
+
const sizeX = Math.max(0.8, layer.shape[2] * 0.01);
|
| 83 |
+
const xPos = currentX + sizeX / 2;
|
| 84 |
+
currentX = xPos + sizeX / 2 + blockGap;
|
| 85 |
+
return { ...layer, size: [sizeX, sizeY, sizeZ], xPos };
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
return { inputPos, mappedLayers, outputPos: [currentX, 0, 0] };
|
| 89 |
+
}, [layerData, previewImage, blockGap]);
|
| 90 |
+
|
| 91 |
+
const activeLayerPanelData = activeTab.type === 'layer'
|
| 92 |
+
? layerData.find(l => l.layer_index === activeTab.layerIndex) || layout.mappedLayers.find(l => l.layer_index === activeTab.layerIndex)
|
| 93 |
+
: null;
|
| 94 |
+
|
| 95 |
+
const handleFileUpload = async (event) => {
|
| 96 |
+
const file = event.target.files[0];
|
| 97 |
+
if (!file) return;
|
| 98 |
+
|
| 99 |
+
cancelAnimationFrame(animationFrameId.current);
|
| 100 |
+
isAwaitingResponse.current = false;
|
| 101 |
+
if (videoRef.current) {
|
| 102 |
+
videoRef.current.pause();
|
| 103 |
+
videoRef.current.removeAttribute('src');
|
| 104 |
+
videoRef.current.load();
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const fileUrl = URL.createObjectURL(file);
|
| 108 |
+
const isVid = file.type.startsWith('video/');
|
| 109 |
+
|
| 110 |
+
setIsVideo(isVid);
|
| 111 |
+
isVideoStateRef.current = isVid;
|
| 112 |
+
setPreviewImage(fileUrl);
|
| 113 |
+
setActiveTab({ type: 'prediction', layerIndex: null });
|
| 114 |
+
setZoomedFeature(null);
|
| 115 |
+
setIsPanelOpen(true);
|
| 116 |
+
|
| 117 |
+
if (isVid) return;
|
| 118 |
+
|
| 119 |
+
setIsProcessing(true);
|
| 120 |
+
const formData = new FormData();
|
| 121 |
+
formData.append("file", file);
|
| 122 |
+
|
| 123 |
+
try {
|
| 124 |
+
const response = await fetch("/predict", {
|
| 125 |
+
method: "POST",
|
| 126 |
+
body: formData,
|
| 127 |
+
});
|
| 128 |
+
const data = await response.json();
|
| 129 |
+
setLayerData(data.layers);
|
| 130 |
+
setPrediction(data.prediction);
|
| 131 |
+
} catch (error) {
|
| 132 |
+
console.error("Error:", error);
|
| 133 |
+
alert("Make sure your FastAPI server is running on localhost:8000!");
|
| 134 |
+
} finally {
|
| 135 |
+
setIsProcessing(false);
|
| 136 |
+
}
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
return (
|
| 140 |
+
<div className="relative w-screen h-screen bg-[#050505] text-white font-sans overflow-hidden">
|
| 141 |
+
|
| 142 |
+
<video
|
| 143 |
+
ref={videoRef}
|
| 144 |
+
src={isVideo && previewImage ? previewImage : undefined}
|
| 145 |
+
className="hidden absolute top-0 left-0 w-0 h-0"
|
| 146 |
+
autoPlay muted loop
|
| 147 |
+
onPlay={() => requestAnimationFrame(processVideoFrame)}
|
| 148 |
+
/>
|
| 149 |
+
<canvas ref={canvasRef} width="227" height="227" className="hidden absolute top-0 left-0 w-0 h-0" />
|
| 150 |
+
|
| 151 |
+
<div className="absolute inset-0 z-0">
|
| 152 |
+
<Canvas
|
| 153 |
+
camera={{ position: [20, 8, 5], fov: 45 }}
|
| 154 |
+
gl={{ antialias: false, powerPreference: "high-performance" }}
|
| 155 |
+
dpr={[1, 1.5]}
|
| 156 |
+
>
|
| 157 |
+
<ambientLight intensity={1.5} />
|
| 158 |
+
<group rotation={[0, Math.PI / 8, 0]}>
|
| 159 |
+
{layout.inputPos && (
|
| 160 |
+
<InputCube position={layout.inputPos} imageUrl={previewImage} isVideo={isVideo} />
|
| 161 |
+
)}
|
| 162 |
+
{layout.mappedLayers.map((layer) => (
|
| 163 |
+
<LayerCube
|
| 164 |
+
key={layer.layer_index}
|
| 165 |
+
position={[layer.xPos, 0, 0]}
|
| 166 |
+
size={layer.size}
|
| 167 |
+
shape={layer.shape}
|
| 168 |
+
textureUrl={layer.texture_b64}
|
| 169 |
+
isSelected={activeTab.type === 'layer' && activeTab.layerIndex === layer.layer_index}
|
| 170 |
+
/>
|
| 171 |
+
))}
|
| 172 |
+
{layout.outputPos && (
|
| 173 |
+
<OutputNode position={layout.outputPos} isSelected={activeTab.type === 'prediction'} />
|
| 174 |
+
)}
|
| 175 |
+
</group>
|
| 176 |
+
<OrbitControls makeDefault enablePan={true} enableZoom={true} target={[0, 0, 0]} />
|
| 177 |
+
</Canvas>
|
| 178 |
+
|
| 179 |
+
{isProcessing && (
|
| 180 |
+
<div className="absolute inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm z-10">
|
| 181 |
+
<h2 className="text-2xl md:text-3xl text-[#00ffcc] font-mono font-bold animate-pulse drop-shadow-lg">Processing Inference...</h2>
|
| 182 |
+
</div>
|
| 183 |
+
)}
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<div className="absolute top-6 left-6 right-16 md:right-auto z-20 pointer-events-none">
|
| 187 |
+
<h1 className="text-2xl md:text-3xl font-black tracking-tight text-white drop-shadow-lg">
|
| 188 |
+
ALEXNET<span className="text-[#00ffcc]"> VISUALIZER</span>
|
| 189 |
+
</h1>
|
| 190 |
+
<p className="text-xs md:text-sm text-gray-300 mt-2 max-w-xs drop-shadow-md leading-relaxed">
|
| 191 |
+
Real-time Convolutional Neural Network inference. Drop an image or video to extract and render 3D architectural feature maps.
|
| 192 |
+
</p>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<div className="absolute bottom-6 left-6 right-6 md:right-auto z-20 bg-[#0f0f0f]/80 backdrop-blur-xl border border-white/10 p-4 md:p-6 rounded-2xl flex flex-col md:flex-row items-stretch md:items-center gap-4 md:gap-8 shadow-2xl">
|
| 196 |
+
<div className="flex justify-between items-center w-full md:w-auto">
|
| 197 |
+
<div>
|
| 198 |
+
<h2 className="text-base md:text-lg font-bold mb-1">Input Source</h2>
|
| 199 |
+
<label className="cursor-pointer bg-white text-black font-bold border border-white px-4 py-2 rounded-lg inline-block text-xs md:text-sm transition-colors hover:bg-gray-200">
|
| 200 |
+
<span>Select File</span>
|
| 201 |
+
<input type="file" className="hidden" accept="image/*,video/*" onChange={handleFileUpload} />
|
| 202 |
+
</label>
|
| 203 |
+
</div>
|
| 204 |
+
{previewImage && (
|
| 205 |
+
<div className="h-16 w-16 md:hidden border border-white/20 rounded-lg overflow-hidden relative shrink-0">
|
| 206 |
+
{isVideo && <div className="absolute top-1 right-1 bg-red-500 w-2 h-2 rounded-full animate-pulse z-10"></div>}
|
| 207 |
+
{isVideo ? <video src={previewImage} className="w-full h-full object-cover" autoPlay muted loop /> : <img src={previewImage} alt="Input" className="w-full h-full object-cover" />}
|
| 208 |
+
</div>
|
| 209 |
+
)}
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
<div className="w-full md:w-48 border-t md:border-t-0 md:border-l border-white/10 pt-4 md:pt-0 md:pl-6">
|
| 213 |
+
<h3 className="text-xs md:text-sm font-semibold text-gray-300 mb-2">Architecture Spacing</h3>
|
| 214 |
+
<input
|
| 215 |
+
type="range"
|
| 216 |
+
min="0.2" max="4.0" step="0.1"
|
| 217 |
+
value={blockGap}
|
| 218 |
+
onChange={(e) => setBlockGap(parseFloat(e.target.value))}
|
| 219 |
+
className="w-full accent-[#00ffcc] cursor-ew-resize"
|
| 220 |
+
/>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
{previewImage && (
|
| 224 |
+
<div className="hidden md:block h-16 w-16 border border-white/20 rounded-lg overflow-hidden relative shrink-0 shadow-lg">
|
| 225 |
+
{isVideo && <div className="absolute top-1 right-1 bg-red-500 w-2 h-2 rounded-full animate-pulse z-10"></div>}
|
| 226 |
+
{isVideo ? <video src={previewImage} className="w-full h-full object-cover" autoPlay muted loop /> : <img src={previewImage} alt="Input" className="w-full h-full object-cover" />}
|
| 227 |
+
</div>
|
| 228 |
+
)}
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
{!isPanelOpen && (
|
| 232 |
+
<button
|
| 233 |
+
onClick={() => setIsPanelOpen(true)}
|
| 234 |
+
className="absolute top-6 right-6 z-30 bg-[#111] border border-white/10 text-[#00ffcc] font-mono font-bold px-4 py-2 rounded-lg shadow-2xl hover:bg-[#222] transition-colors text-xs md:text-sm"
|
| 235 |
+
>
|
| 236 |
+
◀ DATA PANEL
|
| 237 |
+
</button>
|
| 238 |
+
)}
|
| 239 |
+
|
| 240 |
+
{/* FIX: Set md:w-[400px] instead of md-w-100 */}
|
| 241 |
+
<div
|
| 242 |
+
className={`absolute top-0 right-0 bottom-0 md:top-6 md:right-6 md:bottom-6 w-full md:w-[400px] z-40 bg-[#0f0f0f]/95 md:bg-[#0f0f0f]/80 backdrop-blur-2xl md:border border-white/10 md:rounded-2xl flex flex-col shadow-2xl overflow-hidden transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)] ${isPanelOpen ? 'translate-x-0' : 'translate-x-full md:translate-x-[120%]'}`}
|
| 243 |
+
>
|
| 244 |
+
<div className="p-4 md:p-5 border-b border-white/10 bg-black/40 flex justify-between items-start">
|
| 245 |
+
<div>
|
| 246 |
+
<h3 className="text-[10px] md:text-xs text-gray-400 uppercase tracking-widest mb-3 font-semibold">Navigate Architecture</h3>
|
| 247 |
+
<div className="flex flex-wrap gap-2">
|
| 248 |
+
<button
|
| 249 |
+
onClick={() => setActiveTab({ type: 'prediction', layerIndex: null })}
|
| 250 |
+
className={`px-3 py-1.5 text-xs font-bold font-mono rounded border transition-colors ${
|
| 251 |
+
activeTab.type === 'prediction'
|
| 252 |
+
? 'bg-[#f97316] text-black border-[#f97316]'
|
| 253 |
+
: 'bg-[#111] text-gray-400 border-gray-700 hover:bg-[#222]'
|
| 254 |
+
}`}
|
| 255 |
+
>
|
| 256 |
+
Output
|
| 257 |
+
</button>
|
| 258 |
+
{layout.mappedLayers.map((layer) => (
|
| 259 |
+
<button
|
| 260 |
+
key={layer.layer_index}
|
| 261 |
+
onClick={() => setActiveTab({ type: 'layer', layerIndex: layer.layer_index })}
|
| 262 |
+
className={`px-3 py-1.5 text-xs font-bold font-mono rounded border transition-colors ${
|
| 263 |
+
activeTab.type === 'layer' && activeTab.layerIndex === layer.layer_index
|
| 264 |
+
? 'bg-[#00ffcc] text-black border-[#00ffcc]'
|
| 265 |
+
: 'bg-[#111] text-gray-400 border-gray-700 hover:bg-[#222]'
|
| 266 |
+
}`}
|
| 267 |
+
>
|
| 268 |
+
Layer {layer.layer_index}
|
| 269 |
+
</button>
|
| 270 |
+
))}
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
<button
|
| 274 |
+
onClick={() => setIsPanelOpen(false)}
|
| 275 |
+
className="text-gray-500 hover:text-white font-bold ml-2 p-2 rounded-full hover:bg-white/10 transition-colors"
|
| 276 |
+
title="Close Panel"
|
| 277 |
+
>
|
| 278 |
+
✕
|
| 279 |
+
</button>
|
| 280 |
+
</div>
|
| 281 |
+
|
| 282 |
+
<div className="p-6 flex-1 overflow-y-auto relative custom-scrollbar">
|
| 283 |
+
{activeTab.type === 'layer' && activeLayerPanelData && (
|
| 284 |
+
<div className="animate-fadeIn">
|
| 285 |
+
<h2 className="text-xl md:text-2xl font-bold font-mono text-[#00ffcc] mb-4 border-b border-white/10 pb-2">
|
| 286 |
+
Layer {activeLayerPanelData.layer_index}
|
| 287 |
+
</h2>
|
| 288 |
+
<div className="mb-6">
|
| 289 |
+
<p className="text-[10px] md:text-xs text-gray-500 uppercase tracking-wider mb-1">Tensor Shape</p>
|
| 290 |
+
<p className="font-mono text-xs md:text-sm bg-black/50 px-3 py-2 rounded-lg border border-white/10 tracking-widest inline-block text-gray-200">
|
| 291 |
+
{activeLayerPanelData.shape?.join(' × ')}
|
| 292 |
+
</p>
|
| 293 |
+
</div>
|
| 294 |
+
<div>
|
| 295 |
+
<p className="text-[10px] md:text-xs text-gray-500 uppercase tracking-wider mb-2">Activated Feature Maps</p>
|
| 296 |
+
<p className="text-xs text-gray-400 mb-3">Click a specific feature to enlarge.</p>
|
| 297 |
+
|
| 298 |
+
{(() => {
|
| 299 |
+
const channels = activeLayerPanelData.shape?.[2] || 1;
|
| 300 |
+
const maxFeatures = Math.min(channels, 64);
|
| 301 |
+
const gridSize = Math.ceil(Math.sqrt(maxFeatures));
|
| 302 |
+
|
| 303 |
+
return (
|
| 304 |
+
<div
|
| 305 |
+
className="grid gap-[2px] border border-white/10 bg-black/50 p-1 rounded-xl min-h-[200px] relative shadow-inner"
|
| 306 |
+
style={{ gridTemplateColumns: `repeat(${gridSize}, 1fr)` }}
|
| 307 |
+
>
|
| 308 |
+
{Array.from({ length: maxFeatures }).map((_, i) => {
|
| 309 |
+
const col = i % gridSize;
|
| 310 |
+
const row = Math.floor(i / gridSize);
|
| 311 |
+
const bgPosX = gridSize > 1 ? (col / (gridSize - 1)) * 100 : 0;
|
| 312 |
+
const bgPosY = gridSize > 1 ? (row / (gridSize - 1)) * 100 : 0;
|
| 313 |
+
const hasData = !!activeLayerPanelData.texture_b64;
|
| 314 |
+
|
| 315 |
+
return (
|
| 316 |
+
<div
|
| 317 |
+
key={i}
|
| 318 |
+
onClick={() => hasData && setZoomedFeature({ layer: activeLayerPanelData.layer_index, index: i, bgPosX, bgPosY, url: activeLayerPanelData.texture_b64, gridSize })}
|
| 319 |
+
className="aspect-square bg-black/80 rounded-sm cursor-pointer hover:ring-2 hover:ring-[#00ffcc] hover:z-10 transition-all relative"
|
| 320 |
+
style={{
|
| 321 |
+
backgroundImage: hasData ? `url(${activeLayerPanelData.texture_b64})` : 'none',
|
| 322 |
+
backgroundSize: `${gridSize * 100}% ${gridSize * 100}%`,
|
| 323 |
+
backgroundPosition: `${bgPosX}% ${bgPosY}%`,
|
| 324 |
+
imageRendering: 'pixelated'
|
| 325 |
+
}}
|
| 326 |
+
/>
|
| 327 |
+
);
|
| 328 |
+
})}
|
| 329 |
+
</div>
|
| 330 |
+
);
|
| 331 |
+
})()}
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
)}
|
| 335 |
+
|
| 336 |
+
{activeTab.type === 'prediction' && (
|
| 337 |
+
<div className="flex flex-col items-center justify-center h-full text-center pb-10 animate-fadeIn">
|
| 338 |
+
<p className="text-xs text-gray-500 uppercase tracking-wider mb-4 font-semibold">Final Classification</p>
|
| 339 |
+
<div className="bg-black/50 border border-[#f97316]/30 rounded-2xl p-6 md:p-8 w-full shadow-[0_0_30px_rgba(249,115,22,0.1)]">
|
| 340 |
+
{/* FIX: Corrected typo 'warp-break-word' to 'break-words' */}
|
| 341 |
+
<h1 className="text-2xl md:text-3xl font-black text-[#f97316] uppercase tracking-widest break-words drop-shadow-md">
|
| 342 |
+
{prediction || "No Data"}
|
| 343 |
+
</h1>
|
| 344 |
+
</div>
|
| 345 |
+
</div>
|
| 346 |
+
)}
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
{zoomedFeature && (
|
| 351 |
+
<div
|
| 352 |
+
className="absolute inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
|
| 353 |
+
onClick={() => setZoomedFeature(null)}
|
| 354 |
+
>
|
| 355 |
+
<div
|
| 356 |
+
className="bg-[#0f0f0f] border border-white/10 p-6 md:p-8 rounded-2xl shadow-2xl flex flex-col items-center max-w-full"
|
| 357 |
+
onClick={(e) => e.stopPropagation()}
|
| 358 |
+
>
|
| 359 |
+
<h3 className="text-[#00ffcc] font-bold font-mono mb-6 text-lg md:text-xl tracking-widest text-center">
|
| 360 |
+
LAYER {zoomedFeature.layer} <span className="text-gray-600">{"//"}</span> FEATURE {zoomedFeature.index + 1}
|
| 361 |
+
</h3>
|
| 362 |
+
<div
|
| 363 |
+
className="w-64 h-64 md:w-96 md:h-96 border-2 border-white/10 rounded-xl bg-black shadow-inner"
|
| 364 |
+
style={{
|
| 365 |
+
backgroundImage: `url(${zoomedFeature.url})`,
|
| 366 |
+
backgroundSize: `${zoomedFeature.gridSize * 100}% ${zoomedFeature.gridSize * 100}%`,
|
| 367 |
+
backgroundPosition: `${zoomedFeature.bgPosX}% ${zoomedFeature.bgPosY}%`,
|
| 368 |
+
imageRendering: 'pixelated'
|
| 369 |
+
}}
|
| 370 |
+
/>
|
| 371 |
+
<button
|
| 372 |
+
className="mt-8 w-full bg-white hover:bg-gray-200 text-black font-bold py-3 rounded-lg transition-colors text-sm"
|
| 373 |
+
onClick={() => setZoomedFeature(null)}
|
| 374 |
+
>
|
| 375 |
+
Close View
|
| 376 |
+
</button>
|
| 377 |
+
</div>
|
| 378 |
+
</div>
|
| 379 |
+
)}
|
| 380 |
+
</div>
|
| 381 |
+
);
|
| 382 |
+
}
|
frontend/3d-visualizer/src/components/InputCube.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { Suspense } from 'react';
|
| 2 |
+
import { Edges, useTexture, useVideoTexture } from '@react-three/drei';
|
| 3 |
+
import * as THREE from 'three';
|
| 4 |
+
|
| 5 |
+
const ImageMaterial = ({ url }) => {
|
| 6 |
+
const texture = useTexture(url);
|
| 7 |
+
return <meshBasicMaterial map={texture} side={THREE.DoubleSide} />;
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
const VideoMaterial = ({ url }) => {
|
| 11 |
+
const texture = useVideoTexture(url);
|
| 12 |
+
return <meshBasicMaterial map={texture} side={THREE.DoubleSide} />;
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export const InputCube = ({ position, imageUrl, isVideo }) => {
|
| 16 |
+
const sizeX = 0.2;
|
| 17 |
+
const sizeY = 4.5;
|
| 18 |
+
const sizeZ = 4.5;
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<group position={position}>
|
| 22 |
+
<mesh>
|
| 23 |
+
<boxGeometry args={[sizeX, sizeY, sizeZ]} />
|
| 24 |
+
<meshBasicMaterial color="#111" transparent opacity={0.3} side={THREE.DoubleSide} />
|
| 25 |
+
<Edges scale={1.01} color="#ffffff" />
|
| 26 |
+
</mesh>
|
| 27 |
+
<mesh position={[-sizeX / 2 - 0.01, 0, 0]} rotation={[0, -Math.PI / 2, 0]}>
|
| 28 |
+
<planeGeometry args={[sizeZ * 0.98, sizeY * 0.98]} />
|
| 29 |
+
<Suspense fallback={<meshBasicMaterial color="#222" side={THREE.DoubleSide} />}>
|
| 30 |
+
{imageUrl ? (
|
| 31 |
+
isVideo ? <VideoMaterial url={imageUrl} /> : <ImageMaterial url={imageUrl} />
|
| 32 |
+
) : (
|
| 33 |
+
<meshBasicMaterial color="#222" side={THREE.DoubleSide} />
|
| 34 |
+
)}
|
| 35 |
+
</Suspense>
|
| 36 |
+
</mesh>
|
| 37 |
+
</group>
|
| 38 |
+
);
|
| 39 |
+
};
|
frontend/3d-visualizer/src/components/LayerCube.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useMemo, useEffect } from 'react';
|
| 2 |
+
import { Edges } from '@react-three/drei';
|
| 3 |
+
import * as THREE from 'three';
|
| 4 |
+
|
| 5 |
+
const FeatureSlice = ({ texture, gridSize, index, sizeY, sizeZ, xOffset, geometry }) => {
|
| 6 |
+
const sliceTexture = useMemo(() => {
|
| 7 |
+
const cloned = texture.clone();
|
| 8 |
+
cloned.magFilter = THREE.NearestFilter;
|
| 9 |
+
cloned.minFilter = THREE.NearestFilter;
|
| 10 |
+
cloned.repeat.set(1 / gridSize, 1 / gridSize);
|
| 11 |
+
const col = index % gridSize;
|
| 12 |
+
const row = Math.floor(index / gridSize);
|
| 13 |
+
cloned.offset.set(col / gridSize, 1 - (row + 1) / gridSize);
|
| 14 |
+
|
| 15 |
+
if (cloned.image) {
|
| 16 |
+
cloned.needsUpdate = true;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
return cloned;
|
| 20 |
+
}, [texture, gridSize, index]);
|
| 21 |
+
|
| 22 |
+
useEffect(() => {
|
| 23 |
+
return () => {
|
| 24 |
+
sliceTexture.dispose();
|
| 25 |
+
};
|
| 26 |
+
}, [sliceTexture]);
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<mesh position={[xOffset, 0, 0]} rotation={[0, -Math.PI / 2, 0]} geometry={geometry}>
|
| 30 |
+
<meshBasicMaterial
|
| 31 |
+
map={sliceTexture}
|
| 32 |
+
transparent={true}
|
| 33 |
+
opacity={0.8}
|
| 34 |
+
alphaTest={0.1}
|
| 35 |
+
depthWrite={false}
|
| 36 |
+
blending={THREE.AdditiveBlending}
|
| 37 |
+
side={THREE.DoubleSide}
|
| 38 |
+
/>
|
| 39 |
+
</mesh>
|
| 40 |
+
);
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
export const LayerCube = ({ position, size, shape, textureUrl, isSelected }) => {
|
| 44 |
+
const [sizeX, sizeY, sizeZ] = size;
|
| 45 |
+
const channels = shape[2] || 1;
|
| 46 |
+
const numFeatures = Math.min(channels, 24);
|
| 47 |
+
const gridSize = Math.ceil(Math.sqrt(Math.min(channels, 64)));
|
| 48 |
+
|
| 49 |
+
const [imageEl, setImageEl] = useState(null);
|
| 50 |
+
|
| 51 |
+
useEffect(() => {
|
| 52 |
+
if (!textureUrl) return;
|
| 53 |
+
const img = new window.Image();
|
| 54 |
+
img.src = textureUrl;
|
| 55 |
+
img.onload = () => setImageEl(img);
|
| 56 |
+
}, [textureUrl]);
|
| 57 |
+
|
| 58 |
+
const sliceGeometry = useMemo(() => new THREE.PlaneGeometry(sizeZ, sizeY), [sizeZ, sizeY]);
|
| 59 |
+
const fallbackTexture = useMemo(() => new THREE.Texture(), []);
|
| 60 |
+
|
| 61 |
+
const baseTexture = useMemo(() => {
|
| 62 |
+
if (!imageEl) return fallbackTexture;
|
| 63 |
+
const tex = new THREE.Texture(imageEl);
|
| 64 |
+
tex.needsUpdate = true;
|
| 65 |
+
return tex;
|
| 66 |
+
}, [imageEl, fallbackTexture]);
|
| 67 |
+
|
| 68 |
+
return (
|
| 69 |
+
<group position={position}>
|
| 70 |
+
<mesh>
|
| 71 |
+
<boxGeometry args={[sizeX, sizeY, sizeZ]} />
|
| 72 |
+
<meshBasicMaterial color="#050a1f" transparent opacity={0.3} side={THREE.DoubleSide} depthWrite={false} />
|
| 73 |
+
<Edges scale={1.01} threshold={15} color={isSelected ? "#00ffcc" : "#4a90e2"} />
|
| 74 |
+
</mesh>
|
| 75 |
+
|
| 76 |
+
{Array.from({ length: numFeatures }).map((_, index) => {
|
| 77 |
+
const xOffset = (-sizeX / 2) * 0.95 + (sizeX * 0.95 * index) / Math.max(1, numFeatures - 1);
|
| 78 |
+
return (
|
| 79 |
+
<FeatureSlice
|
| 80 |
+
key={index}
|
| 81 |
+
texture={baseTexture}
|
| 82 |
+
gridSize={gridSize}
|
| 83 |
+
index={index}
|
| 84 |
+
sizeY={sizeY}
|
| 85 |
+
sizeZ={sizeZ}
|
| 86 |
+
xOffset={xOffset}
|
| 87 |
+
geometry={sliceGeometry}
|
| 88 |
+
/>
|
| 89 |
+
);
|
| 90 |
+
})}
|
| 91 |
+
</group>
|
| 92 |
+
);
|
| 93 |
+
};
|
frontend/3d-visualizer/src/components/OutputNode.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Edges } from '@react-three/drei';
|
| 3 |
+
|
| 4 |
+
export const OutputNode = ({ position, isSelected }) => {
|
| 5 |
+
return (
|
| 6 |
+
<group position={position}>
|
| 7 |
+
<mesh>
|
| 8 |
+
<boxGeometry args={[0.5, 0.5, 0.5]} />
|
| 9 |
+
<meshBasicMaterial color="#1a1005" transparent opacity={0.8} />
|
| 10 |
+
<Edges scale={1.05} threshold={15} color={isSelected ? "#ffaa00" : "#ff5500"} />
|
| 11 |
+
</mesh>
|
| 12 |
+
<mesh position={[0, 0.6, 0]}>
|
| 13 |
+
<planeGeometry args={[1, 0.3]} />
|
| 14 |
+
<meshBasicMaterial color="black" transparent opacity={0.5} />
|
| 15 |
+
</mesh>
|
| 16 |
+
</group>
|
| 17 |
+
);
|
| 18 |
+
};
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
python-multipart
|
| 4 |
+
numpy
|
| 5 |
+
matplotlib
|
| 6 |
+
tensorflow-cpu
|
| 7 |
+
# cupy-cuda12x
|
src/.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
|
| 5 |
+
venv/
|
| 6 |
+
env/
|
| 7 |
+
.venv/
|
| 8 |
+
|
| 9 |
+
.env
|
| 10 |
+
|
| 11 |
+
*.h5
|
| 12 |
+
*.pkl
|
| 13 |
+
/data/
|
| 14 |
+
|
| 15 |
+
.ipynb_checkpoints
|
| 16 |
+
|
| 17 |
+
.pytest_cache/
|
src/api/api.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
import tensorflow as tf
|
| 5 |
+
import numpy as np
|
| 6 |
+
import cv2
|
| 7 |
+
import base64
|
| 8 |
+
import math
|
| 9 |
+
import os
|
| 10 |
+
import asyncio
|
| 11 |
+
|
| 12 |
+
gpus = tf.config.list_physical_devices('GPU')
|
| 13 |
+
if gpus:
|
| 14 |
+
try:
|
| 15 |
+
for gpu in gpus:
|
| 16 |
+
tf.config.experimental.set_memory_growth(gpu, True)
|
| 17 |
+
except RuntimeError as e:
|
| 18 |
+
print(e)
|
| 19 |
+
|
| 20 |
+
app = FastAPI()
|
| 21 |
+
|
| 22 |
+
app.add_middleware(
|
| 23 |
+
CORSMiddleware,
|
| 24 |
+
allow_origins=["http://localhost:3000"],
|
| 25 |
+
allow_credentials=True,
|
| 26 |
+
allow_methods=["*"],
|
| 27 |
+
allow_headers=["*"],
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
| 31 |
+
|
| 32 |
+
print("Loading AlexNet from scratch...")
|
| 33 |
+
model = tf.keras.models.load_model('alexnet_cifar10_keras.h5')
|
| 34 |
+
|
| 35 |
+
conv_layers = [layer for layer in model.layers if isinstance(layer, tf.keras.layers.Conv2D)]
|
| 36 |
+
feature_extractor = tf.keras.Model(inputs=model.inputs, outputs=[layer.output for layer in conv_layers])
|
| 37 |
+
|
| 38 |
+
CIFAR10_CLASSES = ['Airplane', 'Automobile', 'Bird', 'Cat', 'Deer',
|
| 39 |
+
'Dog', 'Frog', 'Horse', 'Ship', 'Truck']
|
| 40 |
+
|
| 41 |
+
def generate_feature_grid(feature_map, max_features=64):
|
| 42 |
+
if len(feature_map.shape) == 4:
|
| 43 |
+
feature_map = feature_map[0]
|
| 44 |
+
|
| 45 |
+
height, width, channels = feature_map.shape
|
| 46 |
+
num_features = min(channels, max_features)
|
| 47 |
+
grid_size = math.ceil(math.sqrt(num_features))
|
| 48 |
+
|
| 49 |
+
grid_image = np.zeros((grid_size * height, grid_size * width), dtype=np.float32)
|
| 50 |
+
|
| 51 |
+
for i in range(num_features):
|
| 52 |
+
row = i // grid_size
|
| 53 |
+
col = i % grid_size
|
| 54 |
+
|
| 55 |
+
channel_img = feature_map[:, :, i]
|
| 56 |
+
channel_img -= channel_img.min()
|
| 57 |
+
if channel_img.max() > 0:
|
| 58 |
+
channel_img /= channel_img.max()
|
| 59 |
+
channel_img *= 255.0
|
| 60 |
+
|
| 61 |
+
y_start, y_end = row * height, (row + 1) * height
|
| 62 |
+
x_start, x_end = col * width, (col + 1) * width
|
| 63 |
+
grid_image[y_start:y_end, x_start:x_end] = channel_img
|
| 64 |
+
|
| 65 |
+
grid_image = np.uint8(grid_image)
|
| 66 |
+
|
| 67 |
+
colored_grid = cv2.applyColorMap(grid_image, cv2.COLORMAP_VIRIDIS)
|
| 68 |
+
|
| 69 |
+
b_channel, g_channel, r_channel = cv2.split(colored_grid)
|
| 70 |
+
alpha_channel = grid_image # Use the raw grayscale intensity as the alpha map
|
| 71 |
+
|
| 72 |
+
transparent_grid = cv2.merge((b_channel, g_channel, r_channel, alpha_channel))
|
| 73 |
+
|
| 74 |
+
_, buffer = cv2.imencode('.png', transparent_grid)
|
| 75 |
+
return base64.b64encode(buffer).decode('utf-8')
|
| 76 |
+
|
| 77 |
+
@app.post("/predict")
|
| 78 |
+
async def predict_image(file: UploadFile = File(...)):
|
| 79 |
+
contents = await file.read()
|
| 80 |
+
nparr = np.frombuffer(contents, np.uint8)
|
| 81 |
+
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 82 |
+
|
| 83 |
+
img_resized = cv2.resize(img, (227, 227))
|
| 84 |
+
img_normalized = img_resized.astype(np.float32) / 255.0
|
| 85 |
+
img_batch = np.expand_dims(img_normalized, axis=0)
|
| 86 |
+
|
| 87 |
+
activations = feature_extractor.predict(img_batch)
|
| 88 |
+
|
| 89 |
+
predictions = model.predict(img_batch)
|
| 90 |
+
class_idx = np.argmax(predictions[0])
|
| 91 |
+
|
| 92 |
+
layer_data = []
|
| 93 |
+
for i, activation in enumerate(activations):
|
| 94 |
+
b64_image = generate_feature_grid(activation)
|
| 95 |
+
|
| 96 |
+
layer_data.append({
|
| 97 |
+
"layer_index": i + 1,
|
| 98 |
+
"shape": activation.shape[1:],
|
| 99 |
+
"texture_b64": f"data:image/jpeg;base64,{b64_image}"
|
| 100 |
+
})
|
| 101 |
+
|
| 102 |
+
return {
|
| 103 |
+
"prediction": CIFAR10_CLASSES[class_idx],
|
| 104 |
+
"layers": layer_data
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
@app.websocket("/ws/predict-video")
|
| 108 |
+
async def predict_video_stream(websocket: WebSocket):
|
| 109 |
+
await websocket.accept()
|
| 110 |
+
print("WebSocket Connected for Video Stream")
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
while True:
|
| 114 |
+
data = await websocket.receive_text()
|
| 115 |
+
|
| 116 |
+
encoded_data = data.split(',')[1]
|
| 117 |
+
nparr = np.frombuffer(base64.b64decode(encoded_data), np.uint8)
|
| 118 |
+
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 119 |
+
|
| 120 |
+
if img is None:
|
| 121 |
+
continue
|
| 122 |
+
|
| 123 |
+
img_resized = cv2.resize(img, (227, 227))
|
| 124 |
+
img_normalized = img_resized.astype(np.float32) / 255.0
|
| 125 |
+
img_batch = np.expand_dims(img_normalized, axis=0)
|
| 126 |
+
|
| 127 |
+
activations = feature_extractor.predict(img_batch, verbose=0)
|
| 128 |
+
predictions = model.predict(img_batch, verbose=0)
|
| 129 |
+
class_idx = np.argmax(predictions[0])
|
| 130 |
+
|
| 131 |
+
layer_data = []
|
| 132 |
+
for i, activation in enumerate(activations):
|
| 133 |
+
b64_image = generate_feature_grid(activation)
|
| 134 |
+
layer_data.append({
|
| 135 |
+
"layer_index": i + 1,
|
| 136 |
+
"shape": activation.shape[1:],
|
| 137 |
+
"texture_b64": f"data:image/png;base64,{b64_image}"
|
| 138 |
+
})
|
| 139 |
+
|
| 140 |
+
await websocket.send_json({
|
| 141 |
+
"prediction": CIFAR10_CLASSES[class_idx],
|
| 142 |
+
"layers": layer_data
|
| 143 |
+
})
|
| 144 |
+
|
| 145 |
+
await asyncio.sleep(0.01)
|
| 146 |
+
|
| 147 |
+
except WebSocketDisconnect:
|
| 148 |
+
print("WebSocket Disconnected")
|
| 149 |
+
except Exception as e:
|
| 150 |
+
print(f"WebSocket Error: {e}")
|
| 151 |
+
|
| 152 |
+
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
src/core/layers.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cupy as cp
|
| 2 |
+
from utils.im2col import im2col_indices, col2im_indices
|
| 3 |
+
|
| 4 |
+
class Conv2D:
|
| 5 |
+
def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0):
|
| 6 |
+
self.cache = {}
|
| 7 |
+
self.in_channels = in_channels
|
| 8 |
+
self.out_channels = out_channels
|
| 9 |
+
self.kernel_size = kernel_size
|
| 10 |
+
self.stride = stride
|
| 11 |
+
self.padding = padding
|
| 12 |
+
|
| 13 |
+
fan_in = in_channels * kernel_size * kernel_size
|
| 14 |
+
self.weights = cp.random.randn(out_channels, in_channels, kernel_size, kernel_size) * cp.sqrt(2.0 / fan_in)
|
| 15 |
+
self.biases = cp.zeros((out_channels, 1))
|
| 16 |
+
|
| 17 |
+
def forward(self, X):
|
| 18 |
+
n_samples, _, h_in, w_in = X.shape
|
| 19 |
+
|
| 20 |
+
h_out = (h_in - self.kernel_size + 2 * self.padding) // self.stride + 1
|
| 21 |
+
w_out = (w_in - self.kernel_size + 2 * self.padding) // self.stride + 1
|
| 22 |
+
|
| 23 |
+
X_col = im2col_indices(X, self.kernel_size, self.kernel_size, self.padding, self.stride)
|
| 24 |
+
|
| 25 |
+
W_row = self.weights.reshape(self.out_channels, -1)
|
| 26 |
+
|
| 27 |
+
Z_col = W_row @ X_col + self.biases
|
| 28 |
+
|
| 29 |
+
Z = Z_col.reshape(self.out_channels, h_out, w_out, n_samples)
|
| 30 |
+
Z = Z.transpose(3, 0, 1, 2)
|
| 31 |
+
|
| 32 |
+
self.cache['X'] = X
|
| 33 |
+
self.cache['X_col'] = X_col
|
| 34 |
+
self.cache['W_row'] = W_row
|
| 35 |
+
|
| 36 |
+
return Z
|
| 37 |
+
|
| 38 |
+
def backward(self, dZ):
|
| 39 |
+
X = self.cache['X']
|
| 40 |
+
X_col = self.cache['X_col']
|
| 41 |
+
W_row = self.cache['W_row']
|
| 42 |
+
|
| 43 |
+
dZ_col = dZ.transpose(1, 2, 3, 0).reshape(self.out_channels, -1)
|
| 44 |
+
|
| 45 |
+
dW = dZ_col @ X_col.T
|
| 46 |
+
db = cp.sum(dZ_col, axis=1, keepdims=True)
|
| 47 |
+
|
| 48 |
+
dX_col = W_row.T @ dZ_col
|
| 49 |
+
|
| 50 |
+
dX = col2im_indices(dX_col, X.shape, self.kernel_size, self.kernel_size, self.padding, self.stride)
|
| 51 |
+
|
| 52 |
+
self.dW = dW.reshape(self.weights.shape)
|
| 53 |
+
self.db = db
|
| 54 |
+
|
| 55 |
+
return dX
|
| 56 |
+
|
| 57 |
+
class MaxPool2D:
|
| 58 |
+
def __init__(self, pool_size, stride=None):
|
| 59 |
+
self.cache = {}
|
| 60 |
+
self.pool_size = pool_size
|
| 61 |
+
self.stride = stride if stride is not None else pool_size
|
| 62 |
+
|
| 63 |
+
def forward(self, X):
|
| 64 |
+
N, C, H, W = X.shape
|
| 65 |
+
|
| 66 |
+
out_h = (H - self.pool_size) // self.stride + 1
|
| 67 |
+
out_w = (W - self.pool_size) // self.stride + 1
|
| 68 |
+
|
| 69 |
+
X_reshaped = X.reshape(N * C, 1, H, W)
|
| 70 |
+
|
| 71 |
+
X_col = im2col_indices(X_reshaped, self.pool_size, self.pool_size, padding=0, stride=self.stride)
|
| 72 |
+
|
| 73 |
+
max_idx = cp.argmax(X_col, axis=0)
|
| 74 |
+
out = X_col[max_idx, cp.arange(max_idx.size)]
|
| 75 |
+
|
| 76 |
+
out = out.reshape(out_h, out_w, N, C).transpose(2, 3, 0, 1)
|
| 77 |
+
|
| 78 |
+
self.cache['X_shape'] = X.shape
|
| 79 |
+
self.cache['X_col_shape'] = X_col.shape
|
| 80 |
+
self.cache['max_idx'] = max_idx
|
| 81 |
+
|
| 82 |
+
return out
|
| 83 |
+
|
| 84 |
+
def backward(self, dZ):
|
| 85 |
+
X_shape = self.cache['X_shape']
|
| 86 |
+
X_col_shape = self.cache['X_col_shape']
|
| 87 |
+
max_idx = self.cache['max_idx']
|
| 88 |
+
N, C, H, W = X_shape
|
| 89 |
+
|
| 90 |
+
dZ_flat = dZ.transpose(2, 3, 0, 1).ravel()
|
| 91 |
+
|
| 92 |
+
dX_col = cp.zeros(X_col_shape)
|
| 93 |
+
|
| 94 |
+
dX_col[max_idx, cp.arange(max_idx.size)] = dZ_flat
|
| 95 |
+
|
| 96 |
+
dX_reshaped = col2im_indices(dX_col, (N * C, 1, H, W), self.pool_size, self.pool_size, padding=0, stride=self.stride)
|
| 97 |
+
|
| 98 |
+
dX = dX_reshaped.reshape(X_shape)
|
| 99 |
+
|
| 100 |
+
return dX
|
| 101 |
+
|
| 102 |
+
class Flatten:
|
| 103 |
+
def __init__(self):
|
| 104 |
+
self.cache = {}
|
| 105 |
+
|
| 106 |
+
def forward(self, X):
|
| 107 |
+
self.cache['X_shape'] = X.shape
|
| 108 |
+
batch_size = X.shape[0]
|
| 109 |
+
return X.reshape(batch_size, -1)
|
| 110 |
+
|
| 111 |
+
def backward(self, dZ):
|
| 112 |
+
return dZ.reshape(self.cache['X_shape'])
|
| 113 |
+
|
| 114 |
+
class Dropout:
|
| 115 |
+
def __init__(self, p=0.5):
|
| 116 |
+
self.cache = {}
|
| 117 |
+
self.p = p
|
| 118 |
+
self.training = True
|
| 119 |
+
|
| 120 |
+
def forward(self, X):
|
| 121 |
+
if not self.training:
|
| 122 |
+
return X
|
| 123 |
+
|
| 124 |
+
self.mask = (cp.random.rand(*X.shape) > self.p) / (1.0 - self.p)
|
| 125 |
+
return X * self.mask
|
| 126 |
+
|
| 127 |
+
def backward(self, dZ):
|
| 128 |
+
return dZ * self.mask
|
| 129 |
+
|
| 130 |
+
class Linear:
|
| 131 |
+
def __init__(self, input_dimension, output_dimension):
|
| 132 |
+
self.cache = {}
|
| 133 |
+
|
| 134 |
+
self.input_dimension = input_dimension
|
| 135 |
+
self.output_dimension = output_dimension
|
| 136 |
+
|
| 137 |
+
self.weights = cp.random.randn(input_dimension, output_dimension) * cp.sqrt(2.0 / input_dimension)
|
| 138 |
+
self.biases = cp.zeros((1, output_dimension))
|
| 139 |
+
|
| 140 |
+
def forward(self, X):
|
| 141 |
+
Z = X @ self.weights + self.biases
|
| 142 |
+
|
| 143 |
+
self.cache['X'] = X
|
| 144 |
+
|
| 145 |
+
return Z
|
| 146 |
+
|
| 147 |
+
def backward(self, dZ):
|
| 148 |
+
X = self.cache['X']
|
| 149 |
+
|
| 150 |
+
dW = X.T @ dZ
|
| 151 |
+
db = cp.sum(dZ, axis=0, keepdims=True)
|
| 152 |
+
dX = dZ @ self.weights.T
|
| 153 |
+
|
| 154 |
+
self.dW = dW
|
| 155 |
+
self.db = db
|
| 156 |
+
|
| 157 |
+
return dX
|
| 158 |
+
|
| 159 |
+
class ReLU:
|
| 160 |
+
def __init__(self):
|
| 161 |
+
self.cache = {}
|
| 162 |
+
|
| 163 |
+
def forward(self, X):
|
| 164 |
+
self.cache['X'] = X
|
| 165 |
+
return cp.maximum(0, X)
|
| 166 |
+
|
| 167 |
+
def backward(self, dA):
|
| 168 |
+
X = self.cache['X']
|
| 169 |
+
dX = dA * (X > 0)
|
| 170 |
+
return dX
|
src/core/losses.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cupy as cp
|
| 2 |
+
|
| 3 |
+
class CategoricalCrossEntropyLoss:
|
| 4 |
+
def __init__(self):
|
| 5 |
+
self.cache = {}
|
| 6 |
+
|
| 7 |
+
def forward(self, Z, Y):
|
| 8 |
+
"""
|
| 9 |
+
Z: Raw logits from the final Linear layer (batch_size, num_classes)
|
| 10 |
+
Y: True labels, one-hot encoded (batch_size, num_classes)
|
| 11 |
+
"""
|
| 12 |
+
Z_shifted = Z - cp.max(Z, axis=1, keepdims=True)
|
| 13 |
+
exp_Z = cp.exp(Z_shifted)
|
| 14 |
+
probabilities = exp_Z / cp.sum(exp_Z, axis=1, keepdims=True)
|
| 15 |
+
|
| 16 |
+
self.cache['P'] = probabilities
|
| 17 |
+
self.cache['Y'] = Y
|
| 18 |
+
|
| 19 |
+
batch_size = Z.shape[0]
|
| 20 |
+
|
| 21 |
+
P_clipped = cp.clip(probabilities, 1e-8, 1.0 - 1e-8)
|
| 22 |
+
|
| 23 |
+
loss = -cp.sum(Y * cp.log(P_clipped)) / batch_size
|
| 24 |
+
|
| 25 |
+
return loss
|
| 26 |
+
|
| 27 |
+
def backward(self):
|
| 28 |
+
P = self.cache['P']
|
| 29 |
+
Y = self.cache['Y']
|
| 30 |
+
batch_size = P.shape[0]
|
| 31 |
+
|
| 32 |
+
dZ = (P - Y) / batch_size
|
| 33 |
+
|
| 34 |
+
return dZ
|
src/core/optimizers.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cupy as cp
|
| 2 |
+
|
| 3 |
+
class SGDMomentum:
|
| 4 |
+
def __init__(self, layers, learning_rate=0.01, momentum=0.9):
|
| 5 |
+
self.learning_rate = learning_rate
|
| 6 |
+
self.momentum = momentum
|
| 7 |
+
|
| 8 |
+
self.trainable_layers = [layer for layer in layers if hasattr(layer, 'weights')]
|
| 9 |
+
|
| 10 |
+
self.velocities = []
|
| 11 |
+
for layer in self.trainable_layers:
|
| 12 |
+
self.velocities.append({
|
| 13 |
+
'W' : cp.zeros_like(layer.weights),
|
| 14 |
+
'b' : cp.zeros_like(layer.biases)
|
| 15 |
+
})
|
| 16 |
+
|
| 17 |
+
def step(self):
|
| 18 |
+
for idx, layer in enumerate(self.trainable_layers):
|
| 19 |
+
self.velocities[idx]['W'] = (self.momentum * self.velocities[idx]['W']) - (self.learning_rate * layer.dW)
|
| 20 |
+
self.velocities[idx]['b'] = (self.momentum * self.velocities[idx]['b']) - (self.learning_rate * layer.db)
|
| 21 |
+
|
| 22 |
+
layer.weights += self.velocities[idx]['W']
|
| 23 |
+
layer.biases += self.velocities[idx]['b']
|
src/model/alexnet.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from core.layers import Conv2D, MaxPool2D, Flatten, Linear, ReLU, Dropout
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
import cupy as cp
|
| 5 |
+
import numpy as np
|
| 6 |
+
import cv2
|
| 7 |
+
|
| 8 |
+
class Sequential:
|
| 9 |
+
def __init__(self, layers):
|
| 10 |
+
self.layers = layers
|
| 11 |
+
self.criterion = None
|
| 12 |
+
self.optimizer = None
|
| 13 |
+
|
| 14 |
+
def forward(self, X):
|
| 15 |
+
for layer in self.layers:
|
| 16 |
+
X = layer.forward(X)
|
| 17 |
+
return X
|
| 18 |
+
|
| 19 |
+
def backward(self, dZ):
|
| 20 |
+
for layer in reversed(self.layers):
|
| 21 |
+
dZ = layer.backward(dZ)
|
| 22 |
+
return dZ
|
| 23 |
+
|
| 24 |
+
def compile(self, optimizer, criterion):
|
| 25 |
+
self.optimizer = optimizer
|
| 26 |
+
self.criterion = criterion
|
| 27 |
+
|
| 28 |
+
def save_weights(self, filepath="alexnet_weights.pkl"):
|
| 29 |
+
params = []
|
| 30 |
+
for layer in self.layers:
|
| 31 |
+
if hasattr(layer, 'weights'):
|
| 32 |
+
params.append({
|
| 33 |
+
'weights': cp.asnumpy(layer.weights),
|
| 34 |
+
'biases': cp.asnumpy(layer.biases)
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
with open(filepath, 'wb') as f:
|
| 38 |
+
pickle.dump(params, f)
|
| 39 |
+
print(f"Model weights successfully saved to {filepath}")
|
| 40 |
+
|
| 41 |
+
def load_weights(self, filepath="alexnet_weights.pkl"):
|
| 42 |
+
with open(filepath, 'rb') as f:
|
| 43 |
+
params = pickle.load(f)
|
| 44 |
+
|
| 45 |
+
param_idx = 0
|
| 46 |
+
for layer in self.layers:
|
| 47 |
+
if hasattr(layer, 'weights'):
|
| 48 |
+
layer.weights = cp.asarray(params[param_idx]['weights'])
|
| 49 |
+
layer.biases = cp.asarray(params[param_idx]['biases'])
|
| 50 |
+
param_idx += 1
|
| 51 |
+
print(f"Model weights successfully loaded from {filepath}")
|
| 52 |
+
|
| 53 |
+
def fit(self, train_dataset, epochs, val_dataset=None, patience=None, min_delta=0.0):
|
| 54 |
+
if self.optimizer is None or self.criterion is None:
|
| 55 |
+
raise ValueError("Model must be compiled before fitting.")
|
| 56 |
+
|
| 57 |
+
history = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}
|
| 58 |
+
|
| 59 |
+
best_val_loss = float('inf')
|
| 60 |
+
patience_counter = 0
|
| 61 |
+
temp_weights_path = "temp_best_weights.pkl"
|
| 62 |
+
|
| 63 |
+
for epoch in range(epochs):
|
| 64 |
+
epoch_loss = 0.0
|
| 65 |
+
correct_train = 0
|
| 66 |
+
total_train = 0
|
| 67 |
+
|
| 68 |
+
for step, (batch_data, batch_labels) in enumerate(train_dataset):
|
| 69 |
+
X_batch_np = batch_data.numpy().transpose(0, 3, 1, 2)
|
| 70 |
+
Y_batch_np = batch_labels.numpy()
|
| 71 |
+
|
| 72 |
+
X_batch = cp.asarray(X_batch_np)
|
| 73 |
+
Y_batch = cp.asarray(Y_batch_np)
|
| 74 |
+
|
| 75 |
+
predictions = self.forward(X_batch)
|
| 76 |
+
|
| 77 |
+
batch_preds = cp.argmax(predictions, axis=1)
|
| 78 |
+
batch_targets = cp.argmax(Y_batch, axis=1)
|
| 79 |
+
correct_train += int(cp.sum(batch_preds == batch_targets))
|
| 80 |
+
total_train += X_batch.shape[0]
|
| 81 |
+
|
| 82 |
+
loss = self.criterion.forward(predictions, Y_batch)
|
| 83 |
+
epoch_loss += float(loss)
|
| 84 |
+
|
| 85 |
+
dZ = self.criterion.backward()
|
| 86 |
+
self.backward(dZ)
|
| 87 |
+
self.optimizer.step()
|
| 88 |
+
|
| 89 |
+
del X_batch, Y_batch, predictions, dZ
|
| 90 |
+
cp.get_default_memory_pool().free_all_blocks()
|
| 91 |
+
|
| 92 |
+
avg_train_loss = epoch_loss / (step + 1)
|
| 93 |
+
train_acc = correct_train / total_train
|
| 94 |
+
|
| 95 |
+
history['train_loss'].append(avg_train_loss)
|
| 96 |
+
history['train_acc'].append(train_acc)
|
| 97 |
+
|
| 98 |
+
val_print_msg = ""
|
| 99 |
+
if val_dataset is not None:
|
| 100 |
+
val_loss_total = 0.0
|
| 101 |
+
correct_val = 0
|
| 102 |
+
total_val = 0
|
| 103 |
+
|
| 104 |
+
for val_step, (val_data, val_labels) in enumerate(val_dataset):
|
| 105 |
+
X_val = cp.asarray(val_data.numpy().transpose(0, 3, 1, 2))
|
| 106 |
+
Y_val = cp.asarray(val_labels.numpy())
|
| 107 |
+
|
| 108 |
+
val_predictions = self.forward(X_val)
|
| 109 |
+
val_loss_total += float(self.criterion.forward(val_predictions, Y_val))
|
| 110 |
+
|
| 111 |
+
val_preds = cp.argmax(val_predictions, axis=1)
|
| 112 |
+
val_targets = cp.argmax(Y_val, axis=1)
|
| 113 |
+
correct_val += int(cp.sum(val_preds == val_targets))
|
| 114 |
+
total_val += X_val.shape[0]
|
| 115 |
+
|
| 116 |
+
del X_val, Y_val, val_predictions
|
| 117 |
+
cp.get_default_memory_pool().free_all_blocks()
|
| 118 |
+
|
| 119 |
+
avg_val_loss = val_loss_total / (val_step + 1)
|
| 120 |
+
val_acc = correct_val / total_val
|
| 121 |
+
|
| 122 |
+
history['val_loss'].append(avg_val_loss)
|
| 123 |
+
history['val_acc'].append(val_acc)
|
| 124 |
+
|
| 125 |
+
val_print_msg = f" | Val Loss: {avg_val_loss:.4f} | Val Acc: {val_acc:.4f}"
|
| 126 |
+
|
| 127 |
+
if patience is not None:
|
| 128 |
+
if avg_val_loss < (best_val_loss - min_delta):
|
| 129 |
+
best_val_loss = avg_val_loss
|
| 130 |
+
patience_counter = 0
|
| 131 |
+
self.save_weights(temp_weights_path)
|
| 132 |
+
val_print_msg += " (Improved!)"
|
| 133 |
+
else:
|
| 134 |
+
patience_counter += 1
|
| 135 |
+
val_print_msg += f" (Patience: {patience_counter}/{patience})"
|
| 136 |
+
|
| 137 |
+
print(f"Epoch [{epoch + 1}/{epochs}] | Train Loss: {avg_train_loss:.4f} | Train Acc: {train_acc:.4f}{val_print_msg}")
|
| 138 |
+
|
| 139 |
+
if patience is not None and patience_counter >= patience:
|
| 140 |
+
print(f"\nEarly stopping triggered at epoch {epoch + 1}!")
|
| 141 |
+
print("Restoring best model weights...")
|
| 142 |
+
self.load_weights(temp_weights_path)
|
| 143 |
+
break
|
| 144 |
+
|
| 145 |
+
return history
|
| 146 |
+
|
| 147 |
+
def predict(self, X):
|
| 148 |
+
for layer in self.layers:
|
| 149 |
+
if hasattr(layer, 'training'):
|
| 150 |
+
layer.training = False
|
| 151 |
+
|
| 152 |
+
Z = self.forward(X)
|
| 153 |
+
|
| 154 |
+
Z_shifted = Z - cp.max(Z, axis=1, keepdims=True)
|
| 155 |
+
exp_Z = cp.exp(Z_shifted)
|
| 156 |
+
probabilities = exp_Z / cp.sum(exp_Z, axis=1, keepdims=True)
|
| 157 |
+
|
| 158 |
+
predicted_classes = cp.argmax(probabilities, axis=1)
|
| 159 |
+
|
| 160 |
+
for layer in self.layers:
|
| 161 |
+
if hasattr(layer, 'training'):
|
| 162 |
+
layer.training = True
|
| 163 |
+
|
| 164 |
+
return predicted_classes, probabilities
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def build_alexnet():
|
| 168 |
+
model = Sequential([
|
| 169 |
+
Conv2D(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=0),
|
| 170 |
+
ReLU(),
|
| 171 |
+
MaxPool2D(pool_size=3, stride=2),
|
| 172 |
+
|
| 173 |
+
Conv2D(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2),
|
| 174 |
+
ReLU(),
|
| 175 |
+
MaxPool2D(pool_size=3, stride=2),
|
| 176 |
+
|
| 177 |
+
Conv2D(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1),
|
| 178 |
+
ReLU(),
|
| 179 |
+
|
| 180 |
+
Conv2D(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1),
|
| 181 |
+
ReLU(),
|
| 182 |
+
|
| 183 |
+
Conv2D(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1),
|
| 184 |
+
ReLU(),
|
| 185 |
+
|
| 186 |
+
MaxPool2D(pool_size=3, stride=2),
|
| 187 |
+
|
| 188 |
+
Flatten(),
|
| 189 |
+
|
| 190 |
+
Linear(input_dimension=9216, output_dimension=4096),
|
| 191 |
+
ReLU(),
|
| 192 |
+
Dropout(p=0.5),
|
| 193 |
+
|
| 194 |
+
Linear(input_dimension=4096, output_dimension=4096),
|
| 195 |
+
ReLU(),
|
| 196 |
+
Dropout(p=0.5),
|
| 197 |
+
|
| 198 |
+
Linear(input_dimension=4096, output_dimension=10)
|
| 199 |
+
])
|
| 200 |
+
|
| 201 |
+
return model
|
src/trainScratchAlexNet.ipynb
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "ae2fe80c",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [],
|
| 9 |
+
"source": [
|
| 10 |
+
"import cupy as cp\n",
|
| 11 |
+
"import tensorflow as tf\n",
|
| 12 |
+
"import matplotlib.pyplot as plt\n",
|
| 13 |
+
"from model.alexnet import build_alexnet\n",
|
| 14 |
+
"from core.losses import CategoricalCrossEntropyLoss\n",
|
| 15 |
+
"from core.optimizers import SGDMomentum\n",
|
| 16 |
+
"from utils.load_data import load_local_cifar10, preprocess"
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"cell_type": "code",
|
| 21 |
+
"execution_count": 2,
|
| 22 |
+
"id": "428b12cd",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"outputs": [],
|
| 25 |
+
"source": [
|
| 26 |
+
"cifar10_classes = ['Airplane', 'Automobile', 'Bird', 'Cat', 'Deer', \n",
|
| 27 |
+
" 'Dog', 'Frog', 'Horse', 'Ship', 'Truck']"
|
| 28 |
+
]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"cell_type": "code",
|
| 32 |
+
"execution_count": 3,
|
| 33 |
+
"id": "4d6c9de6",
|
| 34 |
+
"metadata": {},
|
| 35 |
+
"outputs": [],
|
| 36 |
+
"source": [
|
| 37 |
+
"(x_train, y_train), (x_test, y_test) = load_local_cifar10('data/cifar-10-batches-py')"
|
| 38 |
+
]
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"cell_type": "code",
|
| 42 |
+
"execution_count": 4,
|
| 43 |
+
"id": "a48175cd",
|
| 44 |
+
"metadata": {},
|
| 45 |
+
"outputs": [],
|
| 46 |
+
"source": [
|
| 47 |
+
"NUM_TRAIN = 1000\n",
|
| 48 |
+
"NUM_VAL = 200"
|
| 49 |
+
]
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"cell_type": "code",
|
| 53 |
+
"execution_count": 5,
|
| 54 |
+
"id": "e9266111",
|
| 55 |
+
"metadata": {},
|
| 56 |
+
"outputs": [
|
| 57 |
+
{
|
| 58 |
+
"name": "stdout",
|
| 59 |
+
"output_type": "stream",
|
| 60 |
+
"text": [
|
| 61 |
+
"Training Data: (1000, 32, 32, 3)\n",
|
| 62 |
+
"Validation Data: (200, 32, 32, 3)\n"
|
| 63 |
+
]
|
| 64 |
+
}
|
| 65 |
+
],
|
| 66 |
+
"source": [
|
| 67 |
+
"x_train = x_train[:NUM_TRAIN]\n",
|
| 68 |
+
"y_train = y_train[:NUM_TRAIN]\n",
|
| 69 |
+
"x_test = x_test[:NUM_VAL]\n",
|
| 70 |
+
"y_test = y_test[:NUM_VAL]\n",
|
| 71 |
+
"\n",
|
| 72 |
+
"print(f\"Training Data: {x_train.shape}\")\n",
|
| 73 |
+
"print(f\"Validation Data: {x_test.shape}\")"
|
| 74 |
+
]
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"cell_type": "code",
|
| 78 |
+
"execution_count": 6,
|
| 79 |
+
"id": "39532a08",
|
| 80 |
+
"metadata": {},
|
| 81 |
+
"outputs": [],
|
| 82 |
+
"source": [
|
| 83 |
+
"y_train = tf.keras.utils.to_categorical(y_train, 10)\n",
|
| 84 |
+
"y_test = tf.keras.utils.to_categorical(y_test, 10)"
|
| 85 |
+
]
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"cell_type": "code",
|
| 89 |
+
"execution_count": 7,
|
| 90 |
+
"id": "13778311",
|
| 91 |
+
"metadata": {},
|
| 92 |
+
"outputs": [],
|
| 93 |
+
"source": [
|
| 94 |
+
"BATCH_SIZE = 16"
|
| 95 |
+
]
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"cell_type": "code",
|
| 99 |
+
"execution_count": 8,
|
| 100 |
+
"id": "9afd3451",
|
| 101 |
+
"metadata": {},
|
| 102 |
+
"outputs": [],
|
| 103 |
+
"source": [
|
| 104 |
+
"train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))\n",
|
| 105 |
+
"train_dataset = (train_dataset\n",
|
| 106 |
+
" .shuffle(NUM_TRAIN)\n",
|
| 107 |
+
" .map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)\n",
|
| 108 |
+
" .batch(BATCH_SIZE)\n",
|
| 109 |
+
" .prefetch(tf.data.AUTOTUNE))"
|
| 110 |
+
]
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"cell_type": "code",
|
| 114 |
+
"execution_count": 9,
|
| 115 |
+
"id": "7cb82693",
|
| 116 |
+
"metadata": {},
|
| 117 |
+
"outputs": [],
|
| 118 |
+
"source": [
|
| 119 |
+
"val_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test))\n",
|
| 120 |
+
"val_dataset = (val_dataset\n",
|
| 121 |
+
" .map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)\n",
|
| 122 |
+
" .batch(BATCH_SIZE)\n",
|
| 123 |
+
" .prefetch(tf.data.AUTOTUNE))"
|
| 124 |
+
]
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"cell_type": "code",
|
| 128 |
+
"execution_count": 10,
|
| 129 |
+
"id": "d8780b77",
|
| 130 |
+
"metadata": {},
|
| 131 |
+
"outputs": [],
|
| 132 |
+
"source": [
|
| 133 |
+
"model = build_alexnet()"
|
| 134 |
+
]
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"cell_type": "code",
|
| 138 |
+
"execution_count": 11,
|
| 139 |
+
"id": "6e68394a",
|
| 140 |
+
"metadata": {},
|
| 141 |
+
"outputs": [],
|
| 142 |
+
"source": [
|
| 143 |
+
"model.compile(\n",
|
| 144 |
+
" optimizer=SGDMomentum(model.layers, learning_rate=0.001, momentum=0.9),\n",
|
| 145 |
+
" criterion=CategoricalCrossEntropyLoss()\n",
|
| 146 |
+
")"
|
| 147 |
+
]
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"cell_type": "code",
|
| 151 |
+
"execution_count": 12,
|
| 152 |
+
"id": "689e8249",
|
| 153 |
+
"metadata": {},
|
| 154 |
+
"outputs": [
|
| 155 |
+
{
|
| 156 |
+
"name": "stdout",
|
| 157 |
+
"output_type": "stream",
|
| 158 |
+
"text": [
|
| 159 |
+
"Model weights successfully saved to temp_best_weights.pkl\n",
|
| 160 |
+
"Epoch [1/50] | Train Loss: 2.4970 | Train Acc: 0.0900 | Val Loss: 2.2824 | Val Acc: 0.1500 (Improved!)\n",
|
| 161 |
+
"Model weights successfully saved to temp_best_weights.pkl\n",
|
| 162 |
+
"Epoch [2/50] | Train Loss: 2.2084 | Train Acc: 0.1800 | Val Loss: 2.1461 | Val Acc: 0.1800 (Improved!)\n",
|
| 163 |
+
"Epoch [3/50] | Train Loss: 2.1496 | Train Acc: 0.1890 | Val Loss: 2.2486 | Val Acc: 0.1850 (Patience: 1/5)\n",
|
| 164 |
+
"Model weights successfully saved to temp_best_weights.pkl\n",
|
| 165 |
+
"Epoch [4/50] | Train Loss: 2.0813 | Train Acc: 0.2350 | Val Loss: 2.1000 | Val Acc: 0.2350 (Improved!)\n",
|
| 166 |
+
"Epoch [5/50] | Train Loss: 1.9815 | Train Acc: 0.2670 | Val Loss: 2.3495 | Val Acc: 0.2150 (Patience: 1/5)\n",
|
| 167 |
+
"Model weights successfully saved to temp_best_weights.pkl\n",
|
| 168 |
+
"Epoch [6/50] | Train Loss: 1.9378 | Train Acc: 0.2950 | Val Loss: 1.9399 | Val Acc: 0.3050 (Improved!)\n",
|
| 169 |
+
"Epoch [7/50] | Train Loss: 1.8588 | Train Acc: 0.3110 | Val Loss: 2.0667 | Val Acc: 0.2050 (Patience: 1/5)\n",
|
| 170 |
+
"Model weights successfully saved to temp_best_weights.pkl\n",
|
| 171 |
+
"Epoch [8/50] | Train Loss: 1.8806 | Train Acc: 0.3190 | Val Loss: 1.9160 | Val Acc: 0.3050 (Improved!)\n",
|
| 172 |
+
"Model weights successfully saved to temp_best_weights.pkl\n",
|
| 173 |
+
"Epoch [9/50] | Train Loss: 1.7617 | Train Acc: 0.3580 | Val Loss: 1.7857 | Val Acc: 0.3400 (Improved!)\n",
|
| 174 |
+
"Epoch [10/50] | Train Loss: 1.7478 | Train Acc: 0.3530 | Val Loss: 1.9351 | Val Acc: 0.2650 (Patience: 1/5)\n",
|
| 175 |
+
"Epoch [11/50] | Train Loss: 1.7252 | Train Acc: 0.3530 | Val Loss: 1.8713 | Val Acc: 0.3300 (Patience: 2/5)\n",
|
| 176 |
+
"Epoch [12/50] | Train Loss: 1.6604 | Train Acc: 0.3970 | Val Loss: 1.8313 | Val Acc: 0.3650 (Patience: 3/5)\n",
|
| 177 |
+
"Epoch [13/50] | Train Loss: 1.6296 | Train Acc: 0.3990 | Val Loss: 1.8864 | Val Acc: 0.3450 (Patience: 4/5)\n",
|
| 178 |
+
"Epoch [14/50] | Train Loss: 1.5955 | Train Acc: 0.4120 | Val Loss: 1.8266 | Val Acc: 0.3250 (Patience: 5/5)\n",
|
| 179 |
+
"\n",
|
| 180 |
+
"Early stopping triggered at epoch 14!\n",
|
| 181 |
+
"Restoring best model weights...\n",
|
| 182 |
+
"Model weights successfully loaded from temp_best_weights.pkl\n"
|
| 183 |
+
]
|
| 184 |
+
}
|
| 185 |
+
],
|
| 186 |
+
"source": [
|
| 187 |
+
"history = model.fit(train_dataset=train_dataset, epochs=50, val_dataset=val_dataset, patience=5)"
|
| 188 |
+
]
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"cell_type": "code",
|
| 192 |
+
"execution_count": 13,
|
| 193 |
+
"id": "ec725200",
|
| 194 |
+
"metadata": {},
|
| 195 |
+
"outputs": [
|
| 196 |
+
{
|
| 197 |
+
"name": "stdout",
|
| 198 |
+
"output_type": "stream",
|
| 199 |
+
"text": [
|
| 200 |
+
"Model weights successfully saved to alexnet_cifar10_weights.pkl\n"
|
| 201 |
+
]
|
| 202 |
+
}
|
| 203 |
+
],
|
| 204 |
+
"source": [
|
| 205 |
+
"model.save_weights(\"alexnet_cifar10_weights.pkl\")"
|
| 206 |
+
]
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"cell_type": "code",
|
| 210 |
+
"execution_count": 14,
|
| 211 |
+
"id": "0b3ca937",
|
| 212 |
+
"metadata": {},
|
| 213 |
+
"outputs": [
|
| 214 |
+
{
|
| 215 |
+
"data": {
|
| 216 |
+
"image/png": "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",
|
| 217 |
+
"text/plain": [
|
| 218 |
+
"<Figure size 1000x400 with 2 Axes>"
|
| 219 |
+
]
|
| 220 |
+
},
|
| 221 |
+
"metadata": {},
|
| 222 |
+
"output_type": "display_data"
|
| 223 |
+
}
|
| 224 |
+
],
|
| 225 |
+
"source": [
|
| 226 |
+
"plt.figure(figsize=(10, 4))\n",
|
| 227 |
+
"\n",
|
| 228 |
+
"plt.subplot(1, 2, 1)\n",
|
| 229 |
+
"plt.plot(history['train_loss'], label='Train Loss', color='blue')\n",
|
| 230 |
+
"if history['val_loss']:\n",
|
| 231 |
+
" plt.plot(history['val_loss'], label='Val Loss', color='orange')\n",
|
| 232 |
+
"plt.title('Cross-Entropy Loss')\n",
|
| 233 |
+
"plt.xlabel('Epoch')\n",
|
| 234 |
+
"plt.legend()\n",
|
| 235 |
+
"\n",
|
| 236 |
+
"plt.subplot(1, 2, 2)\n",
|
| 237 |
+
"plt.plot(history['train_acc'], label='Train Accuracy', color='blue')\n",
|
| 238 |
+
"if history['val_acc']:\n",
|
| 239 |
+
" plt.plot(history['val_acc'], label='Val Accuracy', color='orange')\n",
|
| 240 |
+
"plt.title('Classification Accuracy')\n",
|
| 241 |
+
"plt.xlabel('Epoch')\n",
|
| 242 |
+
"plt.legend()\n",
|
| 243 |
+
"\n",
|
| 244 |
+
"plt.tight_layout()\n",
|
| 245 |
+
"plt.show()"
|
| 246 |
+
]
|
| 247 |
+
}
|
| 248 |
+
],
|
| 249 |
+
"metadata": {
|
| 250 |
+
"kernelspec": {
|
| 251 |
+
"display_name": ".venv",
|
| 252 |
+
"language": "python",
|
| 253 |
+
"name": "python3"
|
| 254 |
+
},
|
| 255 |
+
"language_info": {
|
| 256 |
+
"codemirror_mode": {
|
| 257 |
+
"name": "ipython",
|
| 258 |
+
"version": 3
|
| 259 |
+
},
|
| 260 |
+
"file_extension": ".py",
|
| 261 |
+
"mimetype": "text/x-python",
|
| 262 |
+
"name": "python",
|
| 263 |
+
"nbconvert_exporter": "python",
|
| 264 |
+
"pygments_lexer": "ipython3",
|
| 265 |
+
"version": "3.13.7"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
"nbformat": 4,
|
| 269 |
+
"nbformat_minor": 5
|
| 270 |
+
}
|
src/trainTFalexNet.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/utils/im2col.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cupy as cp
|
| 2 |
+
from cupyx import scatter_add
|
| 3 |
+
|
| 4 |
+
def get_im2col_indices(x_shape, field_height, field_width, padding=1, stride=1):
|
| 5 |
+
"""
|
| 6 |
+
Calculates the indices needed to extract image patches.
|
| 7 |
+
"""
|
| 8 |
+
N, C, H, W = x_shape
|
| 9 |
+
out_height = int((H + 2 * padding - field_height) / stride + 1)
|
| 10 |
+
out_width = int((W + 2 * padding - field_width) / stride + 1)
|
| 11 |
+
|
| 12 |
+
i0 = cp.repeat(cp.arange(field_height), field_width)
|
| 13 |
+
i0 = cp.tile(i0, C)
|
| 14 |
+
i1 = stride * cp.repeat(cp.arange(out_height), out_width)
|
| 15 |
+
j0 = cp.tile(cp.arange(field_width), field_height * C)
|
| 16 |
+
j1 = stride * cp.tile(cp.arange(out_width), out_height)
|
| 17 |
+
|
| 18 |
+
i = i0.reshape(-1, 1) + i1.reshape(1, -1)
|
| 19 |
+
j = j0.reshape(-1, 1) + j1.reshape(1, -1)
|
| 20 |
+
k = cp.repeat(cp.arange(C), field_height * field_width).reshape(-1, 1)
|
| 21 |
+
|
| 22 |
+
return k, i, j
|
| 23 |
+
|
| 24 |
+
def im2col_indices(x, field_height, field_width, padding=1, stride=1):
|
| 25 |
+
"""
|
| 26 |
+
Transforms the 4D image tensor into a 2D matrix of stretched out patches.
|
| 27 |
+
"""
|
| 28 |
+
p = padding
|
| 29 |
+
x_padded = cp.pad(x, ((0, 0), (0, 0), (p, p), (p, p)), mode='constant')
|
| 30 |
+
|
| 31 |
+
k, i, j = get_im2col_indices(x.shape, field_height, field_width, padding, stride)
|
| 32 |
+
|
| 33 |
+
cols = x_padded[:, k, i, j]
|
| 34 |
+
|
| 35 |
+
C = x.shape[1]
|
| 36 |
+
cols = cols.transpose(1, 2, 0).reshape(field_height * field_width * C, -1)
|
| 37 |
+
return cols
|
| 38 |
+
|
| 39 |
+
def col2im_indices(cols, x_shape, field_height, field_width, padding=1, stride=1):
|
| 40 |
+
"""Routes the 2D gradient matrix back into a 4D image tensor."""
|
| 41 |
+
N, C, H, W = x_shape
|
| 42 |
+
H_padded, W_padded = H + 2 * padding, W + 2 * padding
|
| 43 |
+
x_padded = cp.zeros((N, C, H_padded, W_padded), dtype=cols.dtype)
|
| 44 |
+
|
| 45 |
+
k, i, j = get_im2col_indices(x_shape, field_height, field_width, padding, stride)
|
| 46 |
+
|
| 47 |
+
cols_reshaped = cols.reshape(C * field_height * field_width, -1, N)
|
| 48 |
+
cols_reshaped = cols_reshaped.transpose(2, 0, 1)
|
| 49 |
+
|
| 50 |
+
scatter_add(x_padded, (slice(None), k, i, j), cols_reshaped)
|
| 51 |
+
|
| 52 |
+
if padding == 0:
|
| 53 |
+
return x_padded
|
| 54 |
+
return x_padded[:, :, padding:-padding, padding:-padding]
|
src/utils/load_data.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
import pickle
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import pickle
|
| 9 |
+
import numpy as np
|
| 10 |
+
import tensorflow as tf
|
| 11 |
+
|
| 12 |
+
def load_local_cifar10(dataset_dir):
|
| 13 |
+
x_train, y_train = [], []
|
| 14 |
+
|
| 15 |
+
for i in range(1, 6):
|
| 16 |
+
batch_file = os.path.join(dataset_dir, f'data_batch_{i}')
|
| 17 |
+
with open(batch_file, 'rb') as f:
|
| 18 |
+
batch = pickle.load(f, encoding='latin1')
|
| 19 |
+
x_train.append(batch['data'])
|
| 20 |
+
y_train.extend(batch['labels'])
|
| 21 |
+
|
| 22 |
+
x_train = np.vstack(x_train).reshape(-1, 3, 32, 32).transpose(0, 2, 3, 1)
|
| 23 |
+
y_train = np.array(y_train)
|
| 24 |
+
|
| 25 |
+
with open(os.path.join(dataset_dir, 'test_batch'), 'rb') as f:
|
| 26 |
+
batch = pickle.load(f, encoding='latin1')
|
| 27 |
+
x_test = batch['data'].reshape(-1, 3, 32, 32).transpose(0, 2, 3, 1)
|
| 28 |
+
y_test = np.array(batch['labels'])
|
| 29 |
+
|
| 30 |
+
return (x_train, y_train), (x_test, y_test)
|
| 31 |
+
|
| 32 |
+
def preprocess(image, label):
|
| 33 |
+
image = tf.image.resize(image, (227, 227))
|
| 34 |
+
image = tf.cast(image, tf.float32) / 255.0
|
| 35 |
+
return image, label
|