Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BaseRandomLayer Abstract Layer and RandomWidth Preprocessing Layer #7345

Merged
merged 24 commits into from
Mar 15, 2023

Conversation

RWallie
Copy link
Contributor

@RWallie RWallie commented Feb 6, 2023

BaseRandomLayer Abstract Layer and RandomWidth Preprocessing Layer
Co-authored-by:
Natalie Umanzor (@numanzor) [email protected]
Silvia Kocsis (@Silvia42) [email protected]
Ryan Wallace (@RWallie) [email protected]


This change is Reviewable

RWallie and others added 5 commits February 6, 2023 09:35
…ctins for preprocessing layers

Co-authored-by: Silvia Kocsis <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Silvia Kocsis <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
…d handles randomization of width

Co-authored-by: Silvia Kocsis <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Silvia Kocsis <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Silvia Kocsis <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
Copy link
Member

@mattsoulanille mattsoulanille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I have a few questions and changes to suggest.

super(args);
}

protected setRNGType = (rngType: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be a normal function on the class's prototype instead of an arrow function.

Suggested change
protected setRNGType = (rngType: string) => {
protected setRNGType(rngType: string) {

super(args);
}

protected setRNGType = (rngType: string) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the rng type be initialized by an option in the constructor? I think that's how keras does it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TFJS might not support all the keras RNG types, but that should be fine as long as we support pseudorandom and true random.

if (this.seed !== null) {
this.widthFactor = randomUniform([1],
(1.0 + this.widthLower), (1.0 + this.widthUpper),
'float32', this.seed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this.seed is set and never changed, this code will always return the same value. There is no automatic advancing of seeds built in to randomUniform because randomUniform is stateless. You may need to keep track of, and increment, a seed manually.

Comment on lines 140 to 141
this.widthFactor = randomUniform([1],
(1.0 + this.widthLower), (1.0 + this.widthUpper)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if statement checking if seed === null is probably not necessary. You can pass null or undefined as the seed and it will behave the same as if you had not passed anything.

Comment on lines 87 to 91
if(args.seed) {
this.seed = args.seed;
} else {
this.seed = null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if statement prevents people from using the seed 0.

Suggested change
if(args.seed) {
this.seed = args.seed;
} else {
this.seed = null;
}
this.seed = args.seed;

}
this.adjustedWidth = this.widthFactor.dataSync()[0] * imgWidth;
this.adjustedWidth = Math.round(this.adjustedWidth);
const size: [number, number] = [this.imgHeight, this.adjustedWidth];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit: Using as const makes TypeScript correctly infer this as const [number, number] instead of number[].

Suggested change
const size: [number, number] = [this.imgHeight, this.adjustedWidth];
const size = [this.imgHeight, this.adjustedWidth] as const;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resizeBilinear and resizeNearestNeighbor expect a mutable array of two numbers. using as const assigns the readonly type

Comment on lines 22 to 23
const rangeTensor = range(0, 16);
const inputTensor = reshape(rangeTensor, [4,4,1]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit since rangeTensor only seems to be used once.

Suggested change
const rangeTensor = range(0, 16);
const inputTensor = reshape(rangeTensor, [4,4,1]);
const inputTensor = range(0, 16).reshape([4, 4, 1]);

export declare interface BaseRandomLayerArgs extends LayerArgs {}

export type RNGTypes = {
[key: string]: Function;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function is a very wide type here. Can it be narrowed to be more specific?

Comment on lines 29 to 34
private readonly rngTypes: RNGTypes = {
gamma: randomGamma,
normal: randomNormal,
standardNormal: randomStandardNormal,
uniform: randomUniform
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this constant is not specific to this class, it can probably be moved outside of the class. If that's the case, you might also be able to base the RNGTypes type off of this constant.

const RNG_TYPES = {...};
type RNGTypes = typeof RNG_TYPES;

/** @nocollapse */
static className = 'RandomWidth';
randomGenerator: Function;
private rngType: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can reference RNGTypes to be more specific (I know they're both the string type right now, but that may change if the other change I mentioned can be applied).

private rngType: keyof RNGTypes

@RWallie
Copy link
Contributor Author

RWallie commented Feb 8, 2023

Thank you @mattsoulanille for reviewing our pull request. We are getting right on the changes!

Silvia42 and others added 9 commits February 8, 2023 21:31
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
…ss, got rid of rng_types as all random distribution functions seem to be stateless

Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
…nd stores random distribution functions as properties

Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
…mLayer

Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
Co-authored-by: Silvia Kocsis <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Silvia Kocsis <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
@RWallie
Copy link
Contributor Author

RWallie commented Feb 9, 2023

We changed the abstract class layer BaseRandomLayer to map more directly to keras. We created a RandomGenerator class that contains the incrementing of seeds to handle pseudorandomness, as you mentioned. We also stored the random distribution functions as properties on RandomGenerator as they are stored as methods in keras. An instance of RandomGenerator is stored as a property on BaseRandomLayer. We use this instance to directly access randomUniform in the RandomWidth preprocessing layer, as does keras.

We previously had rng_types handling which random distribution would be selected in the preprocessing layers. We removed these - how we had them defined didn't appropriately relate to keras. As you said, in keras, the rng_types were there to determine whether the op was stateful or stateless - we believe all the random distribution functions to be stateless, so we didn't incorporate rng_types when creating an instance of RandomGenerator in BaseRandomLayer.

Some conditionals were removed / rewritten based on your suggestions.

Thanks again @mattsoulanille

@Silvia42
Copy link

Silvia42 commented Feb 9, 2023

tfjs-presubmit (learnjs-174218) failed. The details are not visible for us. Can you please let us know what is the problem? @mattsoulanille

@mattsoulanille
Copy link
Member

Sorry, I think I gave some bad suggestions in my review.

Safari 11.1.2 (Mac OS 10.13.6) RandomWidth Layer webgl1 {"WEBGL_VERSION":1,"WEBGL_CPU_FORWARD":false,"WEBGL_SIZE_UPLOAD_UNIFORM":0} Returns correct, randomly scaled width of Rank 3 Tensor FAILED
	TypeError: inputShape.at is not a function. (In 'inputShape.at(-3)', 'inputShape.at' is undefined) in http://bs-local.com:9876/context.html (line 39696)
	<Jasmine>

Looks like this Safari version does not support Array.at and we're not polyfilling it. You should revert back to what you had before (someArray[someArray.length - someNumber]).

You should be able to access the build logs if you join the discussion or announcement mailing list

Thanks for making the changes! I'll take a look and review them tomorrow.

Silvia42 and others added 2 commits February 9, 2023 22:55
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
@RWallie
Copy link
Contributor Author

RWallie commented Feb 14, 2023

Hey @mattsoulanille hope you are doing well. Just wanted to check in and see how things are looking with our latest changes to the PR. Thanks again!

Copy link
Member

@mattsoulanille mattsoulanille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't get to this sooner.

Comment on lines 18 to 51
import { randomGamma, randomNormal} from '@tensorflow/tfjs-core';
import { randomStandardNormal, randomUniform } from '@tensorflow/tfjs-core';

type randomGammaType = typeof randomGamma;
type randomNormalType = typeof randomNormal;
type randomStandardNormalType = typeof randomStandardNormal;
type randomUniformType = typeof randomUniform;

export class RandomGenerator {
/** @nocollapse */
static className = 'RandomGenerator';
protected currentSeed: number;
private readonly seed: number;
randomGamma: randomGammaType;
randomNormal: randomNormalType;
randomStandardNormal: randomStandardNormalType;
randomUniform: randomUniformType;

constructor(seed: number) {
this.seed = seed;
this.currentSeed = seed;
this.randomGamma = randomGamma;
this.randomNormal = randomNormal;
this.randomStandardNormal = randomStandardNormal;
this.randomUniform = randomUniform;
}

next(): number | null {
if (typeof this.seed === 'number'){
return ++this.currentSeed;
}
return null;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to store the random sampler functions on this class, since they're never called by the class. When you need them somewhere else, you can import them. This class should just keep track of the seed.

Suggested change
import { randomGamma, randomNormal} from '@tensorflow/tfjs-core';
import { randomStandardNormal, randomUniform } from '@tensorflow/tfjs-core';
type randomGammaType = typeof randomGamma;
type randomNormalType = typeof randomNormal;
type randomStandardNormalType = typeof randomStandardNormal;
type randomUniformType = typeof randomUniform;
export class RandomGenerator {
/** @nocollapse */
static className = 'RandomGenerator';
protected currentSeed: number;
private readonly seed: number;
randomGamma: randomGammaType;
randomNormal: randomNormalType;
randomStandardNormal: randomStandardNormalType;
randomUniform: randomUniformType;
constructor(seed: number) {
this.seed = seed;
this.currentSeed = seed;
this.randomGamma = randomGamma;
this.randomNormal = randomNormal;
this.randomStandardNormal = randomStandardNormal;
this.randomUniform = randomUniform;
}
next(): number | null {
if (typeof this.seed === 'number'){
return ++this.currentSeed;
}
return null;
}
}
export class RandomSeed {
private currentSeed: number;
constructor(readonly seed: number) {
this.currentSeed = seed;
}
next(): number {
return ++this.currentSeed;
}
}

Comment on lines 143 to 150
switch (true) {
case this.interpolation === 'bilinear':
return image.resizeBilinear(inputs, size);
case this.interpolation === 'nearest':
return image.resizeNearestNeighbor(inputs, size);
default:
throw new Error(`Interpolation is ${this.interpolation}
but only ${[...INTERPOLATION_METHODS]} are supported`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
switch (true) {
case this.interpolation === 'bilinear':
return image.resizeBilinear(inputs, size);
case this.interpolation === 'nearest':
return image.resizeNearestNeighbor(inputs, size);
default:
throw new Error(`Interpolation is ${this.interpolation}
but only ${[...INTERPOLATION_METHODS]} are supported`);
switch (this.interpolation) {
case 'bilinear':
return image.resizeBilinear(inputs, size);
case 'nearest':
return image.resizeNearestNeighbor(inputs, size);
default:
throw new Error(`Interpolation is ${this.interpolation}
but only ${[...INTERPOLATION_METHODS]} are supported`);

static override className = 'RandomWidth';
private readonly factor: number | [number, number];
private readonly interpolation?: InterpolationType; // defualt = 'bilinear
private seed?: number; // default null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RandomGenerator / RandomSeed should track this seed for you.

Suggested change
private seed?: number; // default null

'float32', this.seed
);

this.seed = this.randomGenerator.next();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When saving this layer, should we save the current seed or the original seed we initialized it with?

this.imgHeight = inputShape[inputShape.length - 3];
const imgWidth = inputShape[inputShape.length - 2];

this.widthFactor = this.randomGenerator.randomUniform([1],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

randomUniform is stateless, so you can just import and call it directly. Same with the other random functions. You don't need to put them on the RandomGenerator class.

Suggested change
this.widthFactor = this.randomGenerator.randomUniform([1],
this.widthFactor = randomUniform([1],

'float32', this.seed
);

this.seed = this.randomGenerator.next();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to avoid duplicating state where possible. The RandomGenerator / RandomSeed class already stores the seed. If you need to check the seed without incrementing it, make it public on the class or add a getter.

Suggested change
this.seed = this.randomGenerator.next();

`);
}

this.seed = seed;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.seed = seed;


return tidy(() => {
const input = getExactlyOneTensor(inputs);
const inputShape = input.shape;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just use input.shape directly. No need to assign a local inputShape variable.

const config: serialization.ConfigDict = {
'factor': this.factor,
'interpolation': this.interpolation,
'seed': this.seed,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseRandomLayer should implement its own getConfig that saves the seed.

Suggested change
'seed': this.seed,

@RWallie
Copy link
Contributor Author

RWallie commented Feb 15, 2023

No worries @mattsoulanille ! Thanks for the review!

Copy link
Collaborator

@chunnienc chunnienc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a closer look after @mattsoulanille 's comments and suggestions are resolved.

factor: number | [number, number];
interpolation?: InterpolationType; // default = 'bilinear';
seed?: number;// default = false;
autoVectorize?:boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space between ':' and 'boolean'. You cloud run clang-format on all your files.

* tf methods unimplemented in tfjs: 'bicubic', 'area', 'lanczos3', 'lanczos5',
* 'gaussian', 'mitchellcubic'
*
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the comment for the class? If so please put it on top of the class.

@RWallie
Copy link
Contributor Author

RWallie commented Feb 17, 2023

We cleaned up the RandomGenerator class and changed it to RandomSeed - It only handles the pseudorandomness now and the random sampler functions will be imported directly into the random preprocessing layers, as we did in random_width.ts.

We took the seed property off of RandomWidth, as you mentioned it's not necessary and is duplicating state. We made the currentSeed inside of RandomSeed public so it would be accessible when calling randomUniform inside the preprocessing layer.

We set up a getConfig() inside of BaseRandomLayer that saves the seed

Thank you @mattsoulanille @chunnienc !

@RWallie
Copy link
Contributor Author

RWallie commented Feb 22, 2023

Hey @mattsoulanille wanted to check in on the PR to see if you had any updates. Thanks!

Comment on lines 23 to 24
protected randomGenerator: RandomSeed;
private seed?: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RandomSeed class already stores the value of seed, so you don't need to store it again here.

Suggested change
protected randomGenerator: RandomSeed;
private seed?: number;
protected randomSeed: RandomSeed;

Comment on lines 28 to 29
this.seed = args.seed;
this.randomGenerator = new RandomSeed(this.seed);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.seed = args.seed;
this.randomGenerator = new RandomSeed(this.seed);
this.randomSeed = new RandomSeed(args.seed);


override getConfig(): serialization.ConfigDict {
const config: serialization.ConfigDict = {
'seed': this.seed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'seed': this.seed
'seed': this.randomSeed.seed,


constructor(args: BaseRandomLayerArgs) {
super(args);
this.randomGenerator = new RandomGenerator(args.seed);
this.seed = args.seed;
this.randomGenerator = new RandomSeed(this.seed);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args.seed has type number | undefined. I think there needs to be a check for undefined here before constructing the RandomSeed (or make seed optional on RandomSeed).

What is the behavior if no seed is passed? Is it true randomness, or does it just choose a seed randomly, or does it choose the same seed every time? If it's the second of these, then I suggest allowing and undefined seed (or no seed) to be passed to RandomSeed. Then, you can have RandomSeed generate a seed randomly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The utility classes for the random sampler classes use Math.random() to assign the seed if none exists. Here's the example from randomUniform's UniformRandom utility class

Comment on lines 126 to 129
'float32', this.randomGenerator.currentSeed
);

this.randomGenerator.next();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.randomGenerator.next() increments the seed and then returns the new seed, so you can use its return value instead of incrementing it separately.

Suggested change
'float32', this.randomGenerator.currentSeed
);
this.randomGenerator.next();
'float32', this.randomGenerator.next());

If you want to use the current seed before incrementing it, change the implementation of RandomGenerator.next to return seed++ (increments after getting the value) instead of ++seed (increments before getting the value).

Silvia42 and others added 3 commits February 23, 2023 12:52
…, and added test for RandomSeed

  Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
Co-authored-by: Natalie Umanzor <[email protected]>
Co-authored-by: Ryan Wallace <[email protected]>
@RWallie
Copy link
Contributor Author

RWallie commented Feb 23, 2023

Hey @mattsoulanille we removed the seed from BaseRandomLayers so we won't be duplicating state. We handled an undefined seed inside of RandomSeed . When calling RandomSeed's next method, we return undefined if the seed is undefined. Randomness gets handled inside of the utility classes for each random sampler function if the seed is null or undefined and generates a seed using Math.random().

@RWallie
Copy link
Contributor Author

RWallie commented Mar 2, 2023

Hey @mattsoulanille - wanted to reach out to see how things are looking with our latest PR. Thank you!

Copy link
Member

@mattsoulanille mattsoulanille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I'm sorry it took me so long to get to this.

Comment on lines 19 to 27
seed: number | undefined;
constructor(seed: number | undefined) {
this.seed = seed;
}
next() {
++this.currentSeed;
next(): number | undefined {
if (this.seed === undefined) {
return undefined;
}
return this.seed++;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to other reviewer: When no seed is specified, the keras implementation does not choose a random seed. It just generates random values. Saving it yields something like this, with the seed set to Null:

{'name': 'random_width_3', 'trainable': True, 'dtype': 'float32', 'factor': (-0.2, 0.3), 'interpolation': 'bilinear', 'seed': None}

This is truly random, and there is no reproducibility here.

On the other hand, when the user sets the seed, it gets saved to the config.

{'name': 'random_width_2', 'trainable': True, 'dtype': 'float32', 'factor': (-0.2, 0.3), 'interpolation': 'bilinear', 'seed': 1}

@AdamLang96
Copy link
Contributor

@mattsoulanille Thanks for all the help on this!

@mattsoulanille
Copy link
Member

@chunnienc Please take a look when you get a chance. Thanks!

@mattsoulanille mattsoulanille merged commit 86e7f4a into tensorflow:master Mar 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants